python generator variable-assignment generator-expression

python - Comportamiento inesperado con una expresión generadora condicional



generator variable-assignment (8)

Como otros han mencionado, los generadores de Python son perezosos. Cuando esta línea se ejecuta:

f = (x for x in array if array.count(x) == 2) # Filters original

En realidad, nada sucede todavía. Acaba de declarar cómo funcionará la función del generador f. Array no se ha visto todavía. Luego, crea una nueva matriz que reemplaza a la primera, y finalmente cuando llama

print(list(f)) # Outputs filtered

el generador ahora necesita los valores reales y comienza a extraerlos del generador f. Pero en este punto, la matriz ya se refiere a la segunda, por lo que obtiene una lista vacía.

Si necesita reasignar la lista y no puede usar una variable diferente para mantenerla, considere crear la lista en lugar de un generador en la segunda línea:

f = [x for x in array if array.count(x) == 2] # Filters original ... print(f)

Estaba ejecutando un fragmento de código que inesperadamente dio un error lógico en una parte del programa. Al investigar la sección, creé un archivo de prueba para probar el conjunto de declaraciones que se estaban ejecutando y descubrí un error inusual que parece muy extraño.

He probado este código simple:

array = [1, 2, 2, 4, 5] # Original array f = (x for x in array if array.count(x) == 2) # Filters original array = [5, 6, 1, 2, 9] # Updates original to something else print(list(f)) # Outputs filtered

Y la salida fue:

>>> []

Si nada. Esperaba que la comprensión del filtro obtuviera los elementos de la matriz con un conteo de 2 y obtuviera esto, pero no obtuve eso:

# Expected output >>> [2, 2]

Cuando comenté la tercera línea para probarlo una vez más:

array = [1, 2, 2, 4, 5] # Original array f = (x for x in array if array.count(x) == 2) # Filters original ### array = [5, 6, 1, 2, 9] # Ignore line print(list(f)) # Outputs filtered

La salida fue correcta (puedes probarlo por ti mismo):

>>> [2, 2]

En un momento dado, escribí el tipo de la variable f :

array = [1, 2, 2, 4, 5] # Original array f = (x for x in array if array.count(x) == 2) # Filters original array = [5, 6, 1, 2, 9] # Updates original print(type(f)) print(list(f)) # Outputs filtered

Y tengo:

>>> <class ''generator''> >>> []

¿Por qué la actualización de una lista en Python está cambiando la salida de otra variable del generador? Esto me parece muy extraño.


La causa raíz del problema es que los generadores son perezosos; Las variables son evaluadas cada vez:

>>> l = [1, 2, 2, 4, 5, 5, 5] >>> filtered = (x for x in l if l.count(x) == 2) >>> l = [1, 2, 4, 4, 5, 6, 6] >>> list(filtered) [4]

Se itera sobre la lista original y evalúa la condición con la lista actual. En este caso, 4 aparecieron dos veces en la nueva lista, haciendo que aparezca en el resultado. Solo aparece una vez en el resultado porque solo apareció una vez en la lista original. Los 6 aparecen dos veces en la nueva lista, pero nunca aparecen en la lista anterior y, por lo tanto, nunca se muestran.

Función completa de introspección para los curiosos (la línea con el comentario es la línea importante):

>>> l = [1, 2, 2, 4, 5] >>> filtered = (x for x in l if l.count(x) == 2) >>> l = [1, 2, 4, 4, 5, 6, 6] >>> list(filtered) [4] >>> def f(original, new, count): current = original filtered = (x for x in current if current.count(x) == count) current = new return list(filtered) >>> from dis import dis >>> dis(f) 2 0 LOAD_FAST 0 (original) 3 STORE_DEREF 1 (current) 3 6 LOAD_CLOSURE 0 (count) 9 LOAD_CLOSURE 1 (current) 12 BUILD_TUPLE 2 15 LOAD_CONST 1 (<code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>) 18 LOAD_CONST 2 (''f.<locals>.<genexpr>'') 21 MAKE_CLOSURE 0 24 LOAD_DEREF 1 (current) 27 GET_ITER 28 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 31 STORE_FAST 3 (filtered) 4 34 LOAD_FAST 1 (new) 37 STORE_DEREF 1 (current) 5 40 LOAD_GLOBAL 0 (list) 43 LOAD_FAST 3 (filtered) 46 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 49 RETURN_VALUE >>> f.__code__.co_varnames (''original'', ''new'', ''count'', ''filtered'') >>> f.__code__.co_cellvars (''count'', ''current'') >>> f.__code__.co_consts (None, <code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3>, ''f.<locals>.<genexpr>'') >>> f.__code__.co_consts[1] <code object <genexpr> at 0x02DD36B0, file "<pyshell#17>", line 3> >>> dis(f.__code__.co_consts[1]) 3 0 LOAD_FAST 0 (.0) >> 3 FOR_ITER 32 (to 38) 6 STORE_FAST 1 (x) 9 LOAD_DEREF 1 (current) # This loads the current list every time, as opposed to loading a constant. 12 LOAD_ATTR 0 (count) 15 LOAD_FAST 1 (x) 18 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 21 LOAD_DEREF 0 (count) 24 COMPARE_OP 2 (==) 27 POP_JUMP_IF_FALSE 3 30 LOAD_FAST 1 (x) 33 YIELD_VALUE 34 POP_TOP 35 JUMP_ABSOLUTE 3 >> 38 LOAD_CONST 0 (None) 41 RETURN_VALUE >>> f.__code__.co_consts[1].co_consts (None,)

Para reiterar: la lista que se va a iterar solo se carga una vez. Sin embargo, todos los cierres en la condición o expresión se cargan desde el ámbito de cierre en cada iteración. No se almacenan en una constante.

La mejor solución para su problema sería crear una nueva variable que haga referencia a la lista original y usarla en su expresión del generador.


La evaluación del generador es "perezosa": no se ejecuta hasta que se actualice con una referencia adecuada. Con tu línea:

Mire nuevamente su salida con el tipo de f : ese objeto es un generador , no una secuencia. Está a la espera de ser utilizado, una especie de iterador.

Su generador no se evalúa hasta que empiece a exigirle valores. En ese punto, utiliza los valores disponibles en ese punto , no el punto en el que se definió.

Código para "hacerlo funcionar"

Eso depende de lo que quieras decir con "haz que funcione". Si desea que f sea ​​una lista filtrada, use una lista, no un generador:

f = [x for x in array if array.count(x) == 2] # Filters original


Las expresiones del generador de Python son vinculantes tardías (ver PEP 289 - Expresiones del generador ) (lo que las otras respuestas llaman "perezoso"):

Unión temprana versus unión tardía

Después de mucha discusión, se decidió que la primera expresión (más externa) para [la expresión del generador] debería evaluarse de inmediato y que las expresiones restantes deben evaluarse cuando se ejecuta el generador.

[...] Python adopta un enfoque de enlace tardío para las expresiones lambda y no tiene precedentes para el enlace temprano y automático. Se consideró que introducir un nuevo paradigma introduciría innecesariamente la complejidad.

Después de explorar muchas posibilidades, surgió el consenso de que los problemas vinculantes eran difíciles de entender y que se debería alentar a los usuarios a usar expresiones generadoras dentro de las funciones que consumen sus argumentos de inmediato. Para aplicaciones más complejas, las definiciones de generadores completos siempre son superiores en términos de ser obvias sobre el alcance, la vida útil y el enlace.

Eso significa que solo evalúa lo más externo al crear la expresión del generador. Entonces, en realidad, enlaza el valor con la array nombres en la "subexpresión" in array (de hecho, en este punto está vinculando el equivalente a iter(array) ). Pero cuando se itera sobre el generador, la llamada if array.count realidad se refiere a lo que actualmente se denomina array .

Como en realidad es una list no una array , cambié los nombres de las variables en el resto de la respuesta para ser más precisos.

En su primer caso, la list que repite y la list que cuenta será diferente. Es como si lo usaras:

list1 = [1, 2, 2, 4, 5] list2 = [5, 6, 1, 2, 9] f = (x for x in list1 if list2.count(x) == 2)

Así que verifica cada elemento en la list1 si su conteo en la list2 es dos.

Puedes verificar esto fácilmente modificando la segunda lista:

>>> lst = [1, 2, 2] >>> f = (x for x in lst if lst.count(x) == 2) >>> lst = [1, 1, 2] >>> list(f) [1]

Si iteraba sobre la primera lista y contaba en la primera lista, habría devuelto [2, 2] (porque la primera lista contiene dos 2 ). Si se repite y se cuenta en la segunda lista, la salida debería ser [1, 1] . Pero como se repite en la primera lista (que contiene un 1 ) pero se comprueba la segunda lista (que contiene dos 1 s), la salida es solo un 1 .

Solución utilizando una función de generador.

Hay varias soluciones posibles, generalmente prefiero no usar "expresiones generadoras" si no se repiten de inmediato. Una simple función de generador será suficiente para que funcione correctamente:

def keep_only_duplicated_items(lst): for item in lst: if lst.count(item) == 2: yield item

Y luego úsalo así:

lst = [1, 2, 2, 4, 5] f = keep_only_duplicated_items(lst) lst = [5, 6, 1, 2, 9] >>> list(f) [2, 2]

Tenga en cuenta que el PEP (ver el enlace anterior) también establece que para algo más complicado es preferible una definición de generador completo.

Una mejor solución utilizando una función de generador con un contador.

Una mejor solución (evitando el comportamiento de tiempo de ejecución cuadrático porque se repite en toda la matriz para cada elemento de la matriz) sería contar ( collections.Counter ) los elementos una vez y luego hacer la búsqueda en tiempo constante (que resulta en tiempo lineal):

from collections import Counter def keep_only_duplicated_items(lst): cnts = Counter(lst) for item in lst: if cnts[item] == 2: yield item

Apéndice: Uso de una subclase para "visualizar" lo que sucede y cuándo sucede

Es bastante fácil crear una subclase de list que se imprime cuando se llaman métodos específicos, por lo que uno puede verificar que realmente funciona así.

En este caso, simplemente __iter__ los métodos __iter__ y count porque me interesa sobre qué lista itera la expresión del generador y en qué lista cuenta. En realidad, los cuerpos de los métodos simplemente delegan en la superclase e imprimen algo (ya que usa super sin argumentos y cadenas de caracteres, requiere Python 3.6, pero debería ser fácil de adaptar para otras versiones de Python):

class MyList(list): def __iter__(self): print(f''__iter__() called on {self!r}'') return super().__iter__() def count(self, item): cnt = super().count(item) print(f''count({item!r}) called on {self!r}, result: {cnt}'') return cnt

Esta es una subclase simple que se imprime cuando se __iter__ los __iter__ y count :

>>> lst = MyList([1, 2, 2, 4, 5]) >>> f = (x for x in lst if lst.count(x) == 2) __iter__() called on [1, 2, 2, 4, 5] >>> lst = MyList([5, 6, 1, 2, 9]) >>> print(list(f)) count(1) called on [5, 6, 1, 2, 9], result: 1 count(2) called on [5, 6, 1, 2, 9], result: 1 count(2) called on [5, 6, 1, 2, 9], result: 1 count(4) called on [5, 6, 1, 2, 9], result: 0 count(5) called on [5, 6, 1, 2, 9], result: 1 []


Los generadores son perezosos y su nueva array definida se usa cuando agota su generador después de redefinirlo. Por lo tanto, la salida es correcta. Una solución rápida es utilizar una comprensión de lista reemplazando los paréntesis () entre corchetes [] .

Pasando a cómo escribir mejor su lógica, contar un valor en un bucle tiene una complejidad cuadrática. Para un algoritmo que funciona en tiempo lineal, puede usar collections.Counter Contador para contar valores y mantener una copia de su lista original :

from collections import Counter array = [1, 2, 2, 4, 5] # original array counts = Counter(array) # count each value in array old_array = array.copy() # make copy array = [5, 6, 1, 2, 9] # updates array # order relevant res = [x for x in old_array if counts[x] >= 2] print(res) # [2, 2] # order irrelevant from itertools import chain res = list(chain.from_iterable([x]*count for x, count in counts.items() if count >= 2)) print(res) # [2, 2]

Note que la segunda versión ni siquiera requiere old_array y es útil si no hay necesidad de mantener el orden de los valores en su matriz original.


Los generadores son perezosos, no serán evaluados hasta que los repitas. En este caso, en el punto en el que se crea la list con el generador como entrada, en la print .


No está utilizando un generador correctamente si este es el uso principal de este código. Use una lista de comprensión en lugar de un generador de comprensión. Solo reemplaza los paréntesis con corchetes. Se evalúa a una lista si no lo sabes.

array = [1, 2, 2, 4, 5] f = [x for x in array if array.count(x) == 2] array = [5, 6, 1, 2, 9] print(f) #[2, 2]

Está recibiendo esta respuesta debido a la naturaleza de un generador. Estás llamando al generador cuando el contenido evaluará a []


Otros ya han explicado la causa raíz del problema: el generador está vinculado al nombre de la variable local de la array , en lugar de a su valor.

La solución más pythonic es definitivamente la lista de comprensión:

f = [x for x in array if array.count(x) == 2]

Sin embargo , si hay alguna razón por la que no desea crear una lista, también puede forzar un alcance cerca de la array :

f = (lambda array=array: (x for x in array if array.count(x) == 2))()

Lo que sucede aquí es que la lambda captura la referencia a la array en el momento en que se ejecuta la línea, asegurándose de que el generador vea la variable que espera, incluso si la variable se redefine más adelante.

Tenga en cuenta que esto aún se une a la variable (referencia), no al valor , por lo que, por ejemplo, se imprimirá [2, 2, 4, 4] :

array = [1, 2, 2, 4, 5] # Original array f = (lambda array=array: (x for x in array if array.count(x) == 2))() # Close over array array.append(4) # This *will* be captured array = [5, 6, 1, 2, 9] # Updates original to something else print(list(f)) # Outputs [2, 2, 4, 4]

Este es un patrón común en algunos idiomas, pero no es muy pitónico, por lo que solo tiene sentido si hay una buena razón para no usar la comprensión de la lista (por ejemplo, si la array es muy larga o si se usa en una comprensión de generador anidado , y te preocupa la memoria).