Enumerar la comprensión vuelve a unir los nombres incluso después del scope de la comprensión. ¿Es esto correcto?

Las comprensiones están teniendo algunas interacciones inesperadas con el scope. ¿Es este el comportamiento esperado?

Tengo un método:

def leave_room(self, uid): u = self.user_by_id(uid) r = self.rooms[u.rid] other_uids = [ouid for ouid in r.users_by_id.keys() if ouid != u.uid] other_us = [self.user_by_id(uid) for uid in other_uids] r.remove_user(uid) # OOPS! uid has been re-bound by the list comprehension above # Interestingly, it's rebound to the last uid in the list, so the error only shows # up when len > 1 

A riesgo de lloriquear, esta es una fuente brutal de errores. A medida que escribo el nuevo código, ocasionalmente encuentro errores muy extraños debido a la reconexión, incluso ahora que sé que es un problema. Necesito establecer una regla como “siempre preceda a las variables temporales en la lista de comprensión con un guión bajo”, pero incluso eso no es infalible.

El hecho de que este tipo de espera aleatoria de una bomba de tiempo anule toda la “facilidad de uso” agradable de las listas de comprensión.

Las comprensiones de la lista filtran la variable de control de bucle en Python 2 pero no en Python 3. Aquí está Guido van Rossum (creador de Python) explicando la historia detrás de esto:

También hicimos otro cambio en Python 3, para mejorar la equivalencia entre las comprensiones de listas y las expresiones generadoras. En Python 2, la comprensión de la lista “filtra” la variable de control de bucle en el ámbito circundante:

 x = 'before' a = [x for x in 1, 2, 3] print x # this prints '3', not 'before' 

Este fue un artefacto de la implementación original de las listas de comprensión; Fue uno de los “pequeños secretos sucios” de Python durante años. Comenzó como un compromiso intencional para hacer que la comprensión de las listas fuera cegadora y, aunque no era un escollo común para los principiantes, definitivamente picaba a las personas de vez en cuando. Para expresiones generadoras no pudimos hacer esto. Las expresiones del generador se implementan utilizando generadores, cuya ejecución requiere un marco de ejecución separado. Por lo tanto, las expresiones generadoras (especialmente si se repiten en una secuencia corta) fueron menos eficientes que las listas de comprensión.

Sin embargo, en Python 3, decidimos arreglar el “pequeño secreto sucio” de las comprensiones de listas usando la misma estrategia de implementación que para las expresiones generadoras. Por lo tanto, en Python 3, el ejemplo anterior (después de la modificación para usar print (x) 🙂 se imprimirá ‘antes’, lo que demuestra que la ‘x’ en la lista de comprensión se oscurece temporalmente pero no anula la ‘x’ en el entorno scope.

Sí, las comprensiones de lista “filtran” su variable en Python 2.x, como en los bucles.

En retrospectiva, esto se reconoció como un error, y se evitó con expresiones generadoras. EDITAR: Como señala Matt B. , también se evitó cuando las syntax de comprensión de diccionario y diccionario se respaldaron desde Python 3.

El comportamiento de la lista de comprensión debía dejarse como está en Python 2, pero está completamente arreglado en Python 3.

Esto significa que en todos:

 list(x for x in a if x>32) set(x//4 for x in a if x>32) # just another generator exp. dict((x, x//16) for x in a if x>32) # yet another generator exp. {x//4 for x in a if x>32} # 2.7+ syntax {x: x//16 for x in a if x>32} # 2.7+ syntax 

la x siempre es local a la expresión mientras que estos:

 [x for x in a if x>32] set([x//4 for x in a if x>32]) # just another list comp. dict([(x, x//16) for x in a if x>32]) # yet another list comp. 

en Python 2.x todos filtran la variable x al ámbito circundante.


ACTUALIZACIÓN para Python 3.8 (?) : PEP 572 introducirá := operador de asignación que se filtra deliberadamente fuera de las expresiones de comprensión y generador! Está motivado esencialmente por dos casos de uso: capturar un “testigo” de funciones de terminación temprana como any() y all() :

 if any((comment := line).startswith('#') for line in lines): print("First comment:", comment) else: print("There are no comments") 

y actualizando estado mutable:

 total = 0 partial_sums = [total := total + v for v in values] 

Vea el Apéndice B para el scope exacto. La variable se asigna en la def circundante más cercana o lambda , a menos que la función lo declare no nonlocal o global .

Sí, la asignación se produce allí, como lo haría en un bucle for . No se está creando ningún nuevo ámbito.

Este es definitivamente el comportamiento esperado: en cada ciclo, el valor está vinculado al nombre que especifique. Por ejemplo,

 >>> x=0 >>> a=[1,54,4,2,32,234,5234,] >>> [x for x in a if x>32] [54, 234, 5234] >>> x 5234 

Una vez que se reconoce, parece bastante fácil evitarlo: no utilice nombres existentes para las variables dentro de las comprensiones.

Curiosamente esto no afecta al diccionario ni establece las comprensiones.

 >>> [x for x in range(1, 10)] [1, 2, 3, 4, 5, 6, 7, 8, 9] >>> x 9 >>> {x for x in range(1, 5)} set([1, 2, 3, 4]) >>> x 9 >>> {x:x for x in range(1, 100)} {1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99} >>> x 9 

Sin embargo, se ha fijado en 3 como se señaló anteriormente.

alguna solución, para Python 2.6, cuando este comportamiento no es deseable

 # python Python 2.6.6 (r266:84292, Aug 9 2016, 06:11:56) Type "help", "copyright", "credits" or "license" for more information. >>> x=0 >>> a=list(x for x in xrange(9)) >>> x 0 >>> a=[x for x in xrange(9)] >>> x 8