La comprensión de listas vuelve a enlazar nombres incluso después del alcance de la comprensión. ¿Es esto correcto?

118

Las comprensiones están teniendo algunas interacciones inesperadas con el alcance. ¿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 brutal fuente de errores. Mientras escribo un código nuevo, de vez en cuando encuentro errores muy extraños debido al reenlace, incluso ahora que sé que es un problema. Necesito hacer una regla como "siempre prefacio vars temporales en listas por comprensión con guión bajo", pero incluso eso no es infalible.

El hecho de que haya esta bomba de tiempo aleatoria en espera niega toda la agradable "facilidad de uso" de las listas por comprensión.

Jabavu Adams
fuente
7
-1: "fuente brutal de errores"? Apenas. ¿Por qué elegir un término tan argumentativo? Generalmente, los errores más costosos son los malentendidos de requisitos y los errores lógicos simples. Este tipo de error ha sido un problema estándar en muchos lenguajes de programación. ¿Por qué llamarlo "brutal"?
S.Lott
44
Viola el principio de mínima sorpresa. Tampoco se menciona en la documentación de Python sobre las listas por comprensión que, sin embargo, menciona varias veces lo fáciles y convenientes que son. Esencialmente, es una mina terrestre que existía fuera de mi modelo de lenguaje y, por lo tanto, me era imposible preverlo.
Jabavu Adams
33
+1 para "fuente brutal de errores". La palabra "brutal" está totalmente justificada.
Nathaniel
3
Lo único "brutal" que veo aquí es su convención de nomenclatura. Ya no estamos en los 80, no estás limitado a nombres de variables de 3 caracteres.
UloPe
5
Nota: el documention hace estado que lista la comprensión son equivalentes a la explícita forconstructo -loop y for-loops variables de fugas . Así que no fue explícito, sino implícitamente declarado.
Bakuriu

Respuestas:

172

Las listas de comprensión 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 listas por comprensión 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 listas por 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 deslumbrantemente rápida, y aunque no era un error común para los principiantes, definitivamente picaba a la gente de vez en cuando. Para las expresiones generadoras no pudimos hacer esto. Las expresiones generadoras se implementan mediante generadores, cuya ejecución requiere un marco de ejecución independiente. Por lo tanto, las expresiones generadoras (especialmente si iteran sobre una secuencia corta) eran menos eficientes que las listas por comprensión.

Sin embargo, en Python 3, decidimos arreglar el "pequeño secreto sucio" de las listas por comprensión 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) :-) imprimirá 'antes', demostrando que la 'x' en la lista de comprensión sombrea temporalmente pero no anula la 'x' en el entorno alcance.

Steven Rumbalski
fuente
14
Agregaré que aunque Guido lo llama un "pequeño secreto sucio", muchos lo consideraron una característica, no un error.
Steven Rumbalski
38
También tenga en cuenta que ahora en 2.7, las comprensiones (y generadores) de conjuntos y diccionarios tienen ámbitos privados, pero las comprensiones de listas todavía no lo tienen. Si bien esto tiene cierto sentido en el sentido de que los primeros fueron respaldados desde Python 3, realmente hace que el contraste con las listas de comprensión sea discordante.
Matt B.
7
Sé que esta es una pregunta increíblemente antigua, pero ¿por qué algunos la consideraron una característica del idioma? ¿Hay algo a favor de este tipo de fuga variable?
Mathias Müller
2
para: bucles con fugas tiene buenas razones, especialmente. para acceder al último valor después de temprano break, pero irrelevante para las comprensiones. Recuerdo algunas discusiones de comp.lang.python donde la gente quería asignar variables en medio de la expresión. La forma menos loca encontrada fue de valor único para las cláusulas, por ejemplo. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1], pero solo necesita una var de comprensión local y funciona igual de bien en Python 3. Creo que "filtrar" era la única forma de establecer una variable visible fuera de una expresión. Todos estuvieron de acuerdo en que estas técnicas son horribles :-)
Beni Cherniavsky-Paskin
1
El problema aquí es no tener acceso al alcance circundante de las comprensiones de la lista, sino vincularse al alcance de las comprensiones de la lista que afecta al alcance circundante.
Felipe Gonçalves Marques
48

Sí, las listas por comprensión "filtran" su variable en Python 2.x, al igual que los bucles for.

En retrospectiva, se reconoció que esto era un error y se evitó con expresiones generadoras. EDITAR: Como señala Matt B. , también se evitó cuando las sintaxis de comprensión de conjuntos y diccionarios se exportaron desde Python 3.

El comportamiento de las listas de comprensión tuvo que 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

el xsiempre es local a la expresión, mientras que éstos:

[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 xvariable al ámbito circundante.


ACTUALIZACIÓN para Python 3.8 (?) : ¡ PEP 572 introducirá un :=operador de asignación que deliberadamente se escapa de las comprensiones y las expresiones del generador! Está motivado esencialmente por 2 casos de uso: capturar un "testigo" de funciones de terminación anticipada 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 el estado mutable:

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

Consulte el Apéndice B para conocer el alcance exacto. La variable se asigna en el entorno más cercano defo lambda, a menos que esa función lo declare nonlocalo global.

Beni Cherniavsky-Paskin
fuente
7

Sí, la asignación ocurre allí, tal como ocurre en un forbucle. No se está creando un 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 de evitar: no use nombres existentes para las variables dentro de las comprensiones.

JAL
fuente
2

Curiosamente, esto no afecta al diccionario ni a la comprensión del conjunto.

>>> [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 corregido en 3 como se indicó anteriormente.

Chris Travers
fuente
Esa sintaxis no funciona en absoluto en Python 2.6. ¿Estás hablando de Python 2.7?
Paul Hollingsworth
Python 2.6 tiene listas por comprensión solo al igual que Python 3.0. 3.1 se agregaron comprensiones de conjunto y diccionario y se transfirieron a 2.7. Lo siento si eso no quedó claro. Estaba destinado a señalar una limitación a otra respuesta, y las versiones a las que se aplica no es del todo sencillo.
Chris Travers
Si bien puedo imaginarme argumentar que hay casos en los que usar python 2.7 para el nuevo código tiene sentido, no puedo decir lo mismo de python 2.6 ... Incluso si 2.6 es lo que vino con su sistema operativo, no está atascado con eso. ¡Considere instalar virtualenv y usar 3.6 para código nuevo!
Alex L
Sin embargo, el punto sobre Python 2.6 podría surgir en el mantenimiento de los sistemas heredados existentes. Entonces, como nota histórica, no es del todo irrelevante. Lo mismo con 3.0 (ick)
Chris Travers
Lo siento si sueno grosero, pero esto no responde a la pregunta de ninguna manera. Es más adecuado como comentario.
0xc0de
1

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
Marek Slebodnik
fuente
-1

En python3, mientras está en la comprensión de la lista, la variable no cambia después de su alcance, pero cuando usamos un bucle for simple, la variable se reasigna fuera del alcance.

i = 1 imprimir (i) imprimir ([i en rango (5)]) imprimir (i) El valor de i seguirá siendo 1 solo.

Ahora solo use simplemente for loop el valor de i será reasignado.

ASHOK KUMAR
fuente