attribute python generator generator-expression

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.