python - tuplas - ceder en listas de comprensiones y expresiones generadoras
metodos de listas en python (1)
Nota : esto fue un error en el manejo del
yield
de CPython en comprensiones y expresiones generadoras, corregido en Python 3.8, con una advertencia de desaprobación en Python 3.7. Consulte el informe de errores de Python y las entradas de Novedades para Python 3.7 y Python 3.8 .
Las expresiones de generador y las comprensiones de conjunto y dictado se compilan en objetos de función (generador). En Python 3, las comprensiones de listas reciben el mismo tratamiento; Todos son, en esencia, un nuevo ámbito anidado.
Puede ver esto si intenta desmontar una expresión generadora:
>>> dis.dis(compile("(i for i in range(3))", '''', ''exec''))
1 0 LOAD_CONST 0 (<code object <genexpr> at 0x10f7530c0, file "", line 1>)
3 LOAD_CONST 1 (''<genexpr>'')
6 MAKE_FUNCTION 0
9 LOAD_NAME 0 (range)
12 LOAD_CONST 2 (3)
15 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
18 GET_ITER
19 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
22 POP_TOP
23 LOAD_CONST 3 (None)
26 RETURN_VALUE
>>> dis.dis(compile("(i for i in range(3))", '''', ''exec'').co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 11 (to 17)
6 STORE_FAST 1 (i)
9 LOAD_FAST 1 (i)
12 YIELD_VALUE
13 POP_TOP
14 JUMP_ABSOLUTE 3
>> 17 LOAD_CONST 0 (None)
20 RETURN_VALUE
Lo anterior muestra que una expresión de generador se compila en un objeto de código, cargada como una función (
MAKE_FUNCTION
crea el objeto de función a partir del objeto de código).
La
.co_consts[0]
nos permite ver el objeto de código generado para la expresión, y usa
YIELD_VALUE
tal como lo haría una función generadora.
Como tal, la expresión de
yield
funciona en ese contexto, ya que el compilador las ve como funciones disfrazadas.
Esto es un error
yield
no tiene lugar en estas expresiones.
La
gramática de
Python antes de Python 3.7 lo permite (por eso el código es compilable), pero la
Reference
muestra que el uso de
yield
aquí no debería funcionar realmente:
La expresión de rendimiento solo se usa al definir una función generadora y, por lo tanto, solo se puede usar en el cuerpo de una definición de función.
Se ha confirmado que se trata de un error en el
problema 10544
.
La resolución del error es que usar el
yield
y el
yield from
generará
un
SyntaxError
en Python 3.8
;
en Python 3.7
, genera una
DeprecationWarning
de
DeprecationWarning
para garantizar que el código deje de usar esta construcción.
Verá la misma advertencia en Python 2.7.15 y versiones posteriores si usa el modificador de
línea de comando
-3
que
habilita las advertencias de compatibilidad con Python 3.
La advertencia 3.7.0b1 se ve así;
convertir las advertencias en errores le ofrece una excepción
SyntaxError
, como lo haría en 3.8:
>>> [(yield i) for i in range(3)]
<stdin>:1: DeprecationWarning: ''yield'' inside list comprehension
<generator object <listcomp> at 0x1092ec7c8>
>>> import warnings
>>> warnings.simplefilter(''error'')
>>> [(yield i) for i in range(3)]
File "<stdin>", line 1
SyntaxError: ''yield'' inside list comprehension
Las diferencias entre cómo funcionan los
yield
en una comprensión de listas y los
yield
en una expresión generadora provienen de las diferencias en cómo se implementan estas dos expresiones.
En Python 3, la comprensión de una lista utiliza llamadas
LIST_APPEND
para agregar la parte superior de la pila a la lista que se está
LIST_APPEND
, mientras que una expresión generadora en su lugar produce ese valor.
Agregar
(yield <expr>)
simplemente agrega otro código de operación
YIELD_VALUE
a:
>>> dis.dis(compile("[(yield i) for i in range(3)]", '''', ''exec'').co_consts[0])
1 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 13 (to 22)
9 STORE_FAST 1 (i)
12 LOAD_FAST 1 (i)
15 YIELD_VALUE
16 LIST_APPEND 2
19 JUMP_ABSOLUTE 6
>> 22 RETURN_VALUE
>>> dis.dis(compile("((yield i) for i in range(3))", '''', ''exec'').co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 12 (to 18)
6 STORE_FAST 1 (i)
9 LOAD_FAST 1 (i)
12 YIELD_VALUE
13 YIELD_VALUE
14 POP_TOP
15 JUMP_ABSOLUTE 3
>> 18 LOAD_CONST 0 (None)
21 RETURN_VALUE
El código de operación
YIELD_VALUE
en los índices de bytecode 15 y 12 respectivamente es extra, un cuco en el nido.
Entonces, para la lista-comprensión-convertida en generador, tiene 1 rendimiento que produce la parte superior de la pila cada vez (reemplazando la parte superior de la pila con el valor de retorno del
yield
), y para la variante de expresión del generador, usted produce la parte superior de la pila ( el número entero) y luego ceder
nuevamente
, pero ahora la pila contiene el valor de retorno del
yield
y no obtienes
None
esa segunda vez.
Para la comprensión de la lista, entonces, la salida del objeto de la
list
prevista aún se devuelve, pero Python 3 ve esto como un generador, por lo que el valor de retorno se adjunta a la
excepción
StopIteration
como el atributo de
value
:
>>> from itertools import islice
>>> listgen = [(yield i) for i in range(3)]
>>> list(islice(listgen, 3)) # avoid exhausting the generator
[0, 1, 2]
>>> try:
... next(listgen)
... except StopIteration as si:
... print(si.value)
...
[None, None, None]
Esos objetos
None
son los valores de retorno de las expresiones de
yield
.
Y para reiterar esto nuevamente;
este mismo problema se aplica al diccionario y la comprensión de conjuntos en Python 2 y Python 3 también;
en Python 2, los valores de retorno de
yield
todavía se agregan al diccionario deseado o al objeto establecido, y el valor de retorno se ''cede'' en último lugar en lugar de adjuntarlo a la excepción
StopIteration
:
>>> list({(yield k): (yield v) for k, v in {''foo'': ''bar'', ''spam'': ''eggs''}.items()})
[''bar'', ''foo'', ''eggs'', ''spam'', {None: None}]
>>> list({(yield i) for i in range(3)})
[0, 1, 2, set([None])]
El siguiente comportamiento me parece bastante intuitivo (Python 3.4):
>>> [(yield i) for i in range(3)]
<generator object <listcomp> at 0x0245C148>
>>> list([(yield i) for i in range(3)])
[0, 1, 2]
>>> list((yield i) for i in range(3))
[0, None, 1, None, 2, None]
Los valores intermedios de la última línea en realidad no siempre son
None
, son lo que
send
al generador, equivalente (supongo) al siguiente generador:
def f():
for i in range(3):
yield (yield i)
Me parece gracioso que esas tres líneas funcionen en absoluto.
La
Reference
dice que el
yield
solo está permitido en una definición de función (aunque puede que lo esté leyendo mal y / o simplemente se haya copiado de la versión anterior).
Las primeras dos líneas producen un
SyntaxError
en Python 2.7, pero la tercera línea no.
Además, parece extraño
- que una comprensión de la lista devuelve un generador y no una lista
- y que la expresión generadora convertida en una lista y la comprensión de la lista correspondiente contienen valores diferentes.
¿Alguien podría proporcionar más información?