python - attribute - Diferencias entre expresiones de comprensión del generador.
title en css (3)
g = [(yield i) for i in range(10)]
Esta construcción acumula los datos que se pasan / pueden devolverse al generador a través de su método send()
y los devuelve a través de la excepción StopIteration
cuando se agota la iteración 1 :
>>> g = [(yield i) for i in range(3)]
>>> next(g)
0
>>> g.send(''abc'')
1
>>> g.send(123)
2
>>> g.send(4.5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [''abc'', 123, 4.5]
>>> # ^^^^^^^^^^^^^^^^^
No sucede tal cosa con la simple comprensión del generador:
>>> g = (i for i in range(3))
>>> next(g)
0
>>> g.send(''abc'')
1
>>> g.send(123)
2
>>> g.send(4.5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
En cuanto al yield from
versión, en Python 3.5 (que estoy usando) no funciona fuera de las funciones, por lo que la ilustración es un poco diferente:
>>> def f(): return [(yield from range(3))]
...
>>> g = f()
>>> next(g)
0
>>> g.send(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in f
AttributeError: ''range_iterator'' object has no attribute ''send''
De acuerdo, send()
no funciona para un generador a from
range()
pero al menos veamos qué hay al final de la iteración:
>>> g = f()
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [None]
>>> # ^^^^^^
1 Tenga en cuenta que incluso si no usa el método send(None)
se supone que se send(None)
, por lo tanto, un generador construido de esta manera siempre usa más memoria que la simple comprensión del generador (ya que debe acumular los resultados de la expresión de yield
hasta el final de la iteración):
>>> g = [(yield i) for i in range(3)]
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [None, None, None]
ACTUALIZAR
Respecto a las diferencias de rendimiento entre las tres variantes. yield from
los otros dos porque elimina un nivel de direccionamiento indirecto (que, a mi entender, es una de las dos razones principales por las que se introdujo el yield from
). Sin embargo, en este ejemplo particular, el yield from
sí mismo es superfluo - g = [(yield from range(10))]
realidad es casi idéntico a g = range(10)
.
Por lo que sé, hay tres formas de crear un generador a través de una comprensión 1 .
El clásico:
def f1():
g = (i for i in range(10))
La variante de yield
:
def f2():
g = [(yield i) for i in range(10)]
El yield from
variante (que genera un SyntaxError
excepto dentro de una función):
def f3():
g = [(yield from range(10))]
Las tres variantes conducen a un código de bytes diferente, lo que no es realmente sorprendente. Parecería lógico que el primero sea el mejor, ya que es una sintaxis dedicada y directa para crear un generador a través de la comprensión. Sin embargo, no es el que produce el bytecode más corto.
Desmontado en Python 3.6
Generador clásico de comprensión.
>>> dis.dis(f1)
4 0 LOAD_CONST 1 (<code object <genexpr> at...>)
2 LOAD_CONST 2 (''f1.<locals>.<genexpr>'')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 STORE_FAST 0 (g)
5 18 LOAD_FAST 0 (g)
20 RETURN_VALUE
variante de yield
>>> dis.dis(f2)
8 0 LOAD_CONST 1 (<code object <listcomp> at...>)
2 LOAD_CONST 2 (''f2.<locals>.<listcomp>'')
4 MAKE_FUNCTION 0
6 LOAD_GLOBAL 0 (range)
8 LOAD_CONST 3 (10)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 STORE_FAST 0 (g)
9 18 LOAD_FAST 0 (g)
20 RETURN_VALUE
yield from
variante
>>> dis.dis(f3)
12 0 LOAD_GLOBAL 0 (range)
2 LOAD_CONST 1 (10)
4 CALL_FUNCTION 1
6 GET_YIELD_FROM_ITER
8 LOAD_CONST 0 (None)
10 YIELD_FROM
12 BUILD_LIST 1
14 STORE_FAST 0 (g)
13 16 LOAD_FAST 0 (g)
18 RETURN_VALUE
Además, una comparación timeit
muestra que el yield from
variante es el más rápido (aún se ejecuta con Python 3.6):
>>> timeit(f1)
0.5334039637357152
>>> timeit(f2)
0.5358906506760719
>>> timeit(f3)
0.19329123352712596
f3
es más o menos 2.7 veces más rápido que f1
y f2
.
Como mencionó León en un comentario, la eficiencia de un generador se mide mejor por la velocidad con la que se puede iterar. Así que cambié las tres funciones para que repitan sobre los generadores y llamen a una función ficticia.
def f():
pass
def fn():
g = ...
for _ in g:
f()
Los resultados son aún más evidentes:
>>> timeit(f1)
1.6017412817975778
>>> timeit(f2)
1.778684261368946
>>> timeit(f3)
0.1960603619517669
f3
ahora es 8.4 veces más rápido que f1
, y 9.3 veces más rápido que f2
.
Nota: los resultados son más o menos los mismos cuando el iterable no es el range(10)
sino un iterable estático, como [0, 1, 2, 3, 4, 5]
. Por lo tanto, la diferencia de velocidad no tiene nada que ver con que el range
se optimice de alguna manera.
Entonces, ¿cuáles son las diferencias entre las tres formas? Más específicamente, ¿cuál es la diferencia entre el yield from
variante y los otros dos?
¿Es este comportamiento normal que el constructo natural (elt for elt in it)
es más lento que el complicado [(yield from it)]
? ¿Debo de ahora en adelante reemplazar el primero por el último en todos mis scripts, o hay algún inconveniente en el uso del yield from
construcción?
Editar
Todo esto está relacionado, así que no tengo ganas de abrir una nueva pregunta, pero esto se está volviendo aún más extraño. Intenté comparar el range(10)
y [(yield from range(10))]
.
def f1():
for i in range(10):
print(i)
def f2():
for i in [(yield from range(10))]:
print(i)
>>> timeit(f1, number=100000)
26.715589237537195
>>> timeit(f2, number=100000)
0.019948781941049987
Asi que. Ahora, la iteración sobre [(yield from range(10))]
es 186 veces más rápida que la iteración sobre un range(10)
desnudo range(10)
?
¿Cómo explica por qué la iteración sobre [(yield from range(10))]
es mucho más rápida que la iteración sobre el range(10)
?
1: Para los escépticos, las tres expresiones siguientes producen un objeto generator
; Intenta y llama type
en ellos.
Esto es lo que deberías estar haciendo:
g = (i for i in range(10))
Es una expresión generadora. Es equivalente a
def temp(outer):
for i in outer:
yield i
g = temp(range(10))
pero si solo quisieras un iterable con los elementos de range(10)
, podrías haber hecho
g = range(10)
No es necesario envolver nada de esto en una función.
Si estás aquí para aprender qué código escribir, puedes dejar de leer. El resto de esta publicación es una explicación larga y técnica de por qué los otros fragmentos de código están rotos y no se deben usar, incluida una explicación de por qué los tiempos también están rotos.
Esta:
g = [(yield i) for i in range(10)]
Es una construcción rota que debería haber sido sacada hace años. 8 años después de que el problema se notificó originalmente , el proceso para eliminarlo finalmente comienza . No lo hagas
Si bien todavía está en el idioma, en Python 3, es equivalente a
def temp(outer):
l = []
for i in outer:
l.append((yield i))
return l
g = temp(range(10))
Se supone que las comprensiones de listas devuelven listas, pero debido al yield
, esta no lo hace. Actúa como una expresión generadora, y produce las mismas cosas que su primer fragmento, pero crea una lista innecesaria y la adjunta a la StopIteration
al final.
>>> g = [(yield i) for i in range(10)]
>>> [next(g) for i in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: [None, None, None, None, None, None, None, None, None, None]
Esto es confuso y un desperdicio de memoria. No lo hagas (Si desea saber de dónde provienen todos esos None
, lea PEP 342 ).
En Python 2, g = [(yield i) for i in range(10)]
hace algo completamente diferente. Python 2 no le da a las comprensiones de listas su propio alcance, específicamente las comprensiones de listas, no las dictados o definidas, por lo que el yield
se ejecuta mediante cualquier función que contenga esta línea. En Python 2, esto:
def f():
g = [(yield i) for i in range(10)]
es equivalente a
def f():
temp = []
for i in range(10):
temp.append((yield i))
g = temp
Haciendo f
una corutina basada en un generador, en el sentido pre-asíncrono . Nuevamente, si su objetivo era obtener un generador, ha perdido un montón de tiempo creando una lista sin sentido.
Esta:
g = [(yield from range(10))]
es una tontería, pero esta vez la culpa no está en Python.
No hay comprensión o genexp aquí en absoluto. Los paréntesis no son una lista de comprensión; todo el trabajo se realiza por yield from
, y luego se construye una lista de 1 elemento que contiene el valor de retorno (inútil) del yield from
. Tu f3
:
def f3():
g = [(yield from range(10))]
cuando se despoja de la innecesaria construcción de listas, se simplifica a
def f3():
yield from range(10)
o, haciendo caso omiso de todas las cosas de soporte de coroutine hace,
def f3():
for i in range(10):
yield i
Tus tiempos también están rotos.
En su primera sincronización, f1
y f2
crean objetos generadores que pueden usarse dentro de esas funciones, aunque el generador de f2
es extraño. f3
no hace eso; f3
es una función de generador. El cuerpo de f3
no se ejecuta en tus tiempos, y si lo hiciera, su g
se comportaría de manera muy diferente a la de las otras funciones g
. Un tiempo que en realidad sería comparable con f1
y f2
sería
def f4():
g = f3()
En su segunda sincronización, f2
no se ejecuta realmente, por la misma razón que f3
se rompió en la sincronización anterior. En su segunda sincronización, f2
no está iterando sobre un generador. En cambio, el yield from
convierte f2
en una función de generador en sí.
Esto podría no hacer lo que crees que hace.
def f2():
for i in [(yield from range(10))]:
print(i)
Llámalo:
>>> def f2():
... for i in [(yield from range(10))]:
... print(i)
...
>>> f2() #Doesn''t print.
<generator object f2 at 0x02C0DF00>
>>> set(f2()) #Prints `None`, because `(yield from range(10))` evaluates to `None`.
None
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
Debido a que el yield from
no está dentro de una comprensión, está vinculado a la función f2
lugar de una función implícita, convirtiendo f2
en una función generadora.
Recordé haber visto a alguien señalar que en realidad no estaba iterando, pero no recuerdo dónde lo vi. Yo mismo estaba probando el código cuando redescubrí esto. No encontré la fuente buscando en la publicación de la lista de correo ni en el subproceso de seguimiento de errores . Si alguien encuentra la fuente, por favor dígame o agréguela a la publicación en sí, para que pueda ser acreditada.