En Python, ¿hay alguna diferencia entre crear un objeto generador a través de una expresión generadora y usar la declaración de rendimiento ?
Usando rendimiento :
def Generator(x, y):
for i in xrange(x):
for j in xrange(y):
yield(i, j)
Usando expresión generadora :
def Generator(x, y):
return ((i, j) for i in xrange(x) for j in xrange(y))
Ambas funciones devuelven objetos generadores, que producen tuplas, por ejemplo (0,0), (0,1), etc.
¿Alguna ventaja de uno u otro? Pensamientos
¡Gracias a todos! ¡Hay mucha información excelente y más referencias en estas respuestas!
python
python-3.x
generator
yield
cschol
fuente
fuente
Respuestas:
Solo hay ligeras diferencias entre los dos. Puede utilizar el
dis
módulo para examinar este tipo de cosas por sí mismo.Editar: Mi primera versión descompiló la expresión generadora creada en el alcance del módulo en el indicador interactivo. Eso es ligeramente diferente de la versión del OP que se usa dentro de una función. Modifiqué esto para que coincida con el caso real de la pregunta.
Como puede ver a continuación, el generador de "rendimiento" (primer caso) tiene tres instrucciones adicionales en la configuración, pero desde el principio
FOR_ITER
se diferencian sólo en un aspecto: el enfoque de "rendimiento" usa aLOAD_FAST
en lugar de aLOAD_DEREF
dentro del ciclo. ElLOAD_DEREF
es "bastante más lento" queLOAD_FAST
, por lo que hace que la versión "rendimiento" sea un poco más rápida que la expresión del generador para valores suficientemente grandes dex
(el ciclo externo) porque el valor dey
se carga un poco más rápido en cada pasada. Para valores más pequeñosx
, sería un poco más lento debido a la sobrecarga adicional del código de configuración.También podría valer la pena señalar que la expresión del generador generalmente se usaría en línea en el código, en lugar de envolverla con la función de esa manera. Eso eliminaría un poco la sobrecarga de configuración y mantendría la expresión del generador un poco más rápida para valores de bucle más pequeños, incluso si
LOAD_FAST
le diera una ventaja a la versión de "rendimiento".En ningún caso la diferencia de desempeño sería suficiente para justificar la decisión entre uno u otro. La legibilidad cuenta mucho más, así que use el que se sienta más legible para la situación en cuestión.
>>> def Generator(x, y): ... for i in xrange(x): ... for j in xrange(y): ... yield(i, j) ... >>> dis.dis(Generator) 2 0 SETUP_LOOP 54 (to 57) 3 LOAD_GLOBAL 0 (xrange) 6 LOAD_FAST 0 (x) 9 CALL_FUNCTION 1 12 GET_ITER >> 13 FOR_ITER 40 (to 56) 16 STORE_FAST 2 (i) 3 19 SETUP_LOOP 31 (to 53) 22 LOAD_GLOBAL 0 (xrange) 25 LOAD_FAST 1 (y) 28 CALL_FUNCTION 1 31 GET_ITER >> 32 FOR_ITER 17 (to 52) 35 STORE_FAST 3 (j) 4 38 LOAD_FAST 2 (i) 41 LOAD_FAST 3 (j) 44 BUILD_TUPLE 2 47 YIELD_VALUE 48 POP_TOP 49 JUMP_ABSOLUTE 32 >> 52 POP_BLOCK >> 53 JUMP_ABSOLUTE 13 >> 56 POP_BLOCK >> 57 LOAD_CONST 0 (None) 60 RETURN_VALUE >>> def Generator_expr(x, y): ... return ((i, j) for i in xrange(x) for j in xrange(y)) ... >>> dis.dis(Generator_expr.func_code.co_consts[1]) 2 0 SETUP_LOOP 47 (to 50) 3 LOAD_FAST 0 (.0) >> 6 FOR_ITER 40 (to 49) 9 STORE_FAST 1 (i) 12 SETUP_LOOP 31 (to 46) 15 LOAD_GLOBAL 0 (xrange) 18 LOAD_DEREF 0 (y) 21 CALL_FUNCTION 1 24 GET_ITER >> 25 FOR_ITER 17 (to 45) 28 STORE_FAST 2 (j) 31 LOAD_FAST 1 (i) 34 LOAD_FAST 2 (j) 37 BUILD_TUPLE 2 40 YIELD_VALUE 41 POP_TOP 42 JUMP_ABSOLUTE 25 >> 45 POP_BLOCK >> 46 JUMP_ABSOLUTE 6 >> 49 POP_BLOCK >> 50 LOAD_CONST 0 (None) 53 RETURN_VALUE
fuente
LOAD_DEREF
es "bastante más lenta", por lo que si el rendimiento realmente importaratimeit
, sería bueno el momento real . Un análisis teórico sólo llega hasta cierto punto.En este ejemplo, no realmente. Pero
yield
se puede usar para construcciones más complejas; por ejemplo, también puede aceptar valores de la persona que llama y modificar el flujo como resultado. Lea PEP 342 para obtener más detalles (es una técnica interesante que vale la pena conocer).De todos modos, el mejor consejo es utilizar lo que sea más claro para sus necesidades .
PD Aquí hay un ejemplo de corrutina simple de Dave Beazley :
def grep(pattern): print "Looking for %s" % pattern while True: line = (yield) if pattern in line: print line, # Example use if __name__ == '__main__': g = grep("python") g.next() g.send("Yeah, but no, but yeah, but no") g.send("A series of tubes") g.send("python generators rock!")
fuente
No hay diferencia para el tipo de bucles simples que puede encajar en una expresión generadora. Sin embargo, el rendimiento se puede utilizar para crear generadores que realicen un procesamiento mucho más complejo. Aquí hay un ejemplo simple para generar la secuencia de fibonacci:
>>> def fibgen(): ... a = b = 1 ... while True: ... yield a ... a, b = b, a+b >>> list(itertools.takewhile((lambda x: x<100), fibgen())) [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
fuente
En uso, observe una distinción entre un objeto generador y una función de generador.
Un objeto generador se puede usar una sola vez, en contraste con una función de generador, que se puede reutilizar cada vez que se vuelve a llamar, porque devuelve un objeto generador nuevo.
En la práctica, las expresiones generadoras se suelen utilizar "sin procesar", sin incluirlas en una función, y devuelven un objeto generador.
P.ej:
def range_10_gen_func(): x = 0 while x < 10: yield x x = x + 1 print(list(range_10_gen_func())) print(list(range_10_gen_func())) print(list(range_10_gen_func()))
que salidas:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Compare con un uso ligeramente diferente:
que salidas:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [] []
Y compárelo con una expresión generadora:
range_10_gen_expr = (x for x in range(10)) print(list(range_10_gen_expr)) print(list(range_10_gen_expr)) print(list(range_10_gen_expr))
que también genera:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [] []
fuente
Usar
yield
es bueno si la expresión es más complicada que solo bucles anidados. Entre otras cosas, puede devolver un primer valor especial o un último valor especial. Considerar:def Generator(x): for i in xrange(x): yield(i) yield(None)
fuente
Al pensar en iteradores, el
itertools
módulo:Para el rendimiento, considere
itertools.product(*iterables[, repeat])
>>> import itertools >>> def gen(x,y): ... return itertools.product(xrange(x),xrange(y)) ... >>> [t for t in gen(3,2)] [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)] >>>
fuente
Sí, hay una diferencia.
Para la expresión generadora
(x for var in expr)
,iter(expr)
se llama cuando se crea la expresión .Al usar
def
yyield
para crear un generador, como en:def my_generator(): for var in expr: yield x g = my_generator()
iter(expr)
aún no está llamado. Solo se llamará al iterarg
(y es posible que no se llame en absoluto).Tomando este iterador como ejemplo:
from __future__ import print_function class CountDown(object): def __init__(self, n): self.n = n def __iter__(self): print("ITER") return self def __next__(self): if self.n == 0: raise StopIteration() self.n -= 1 return self.n next = __next__ # for python2
Este código:
g1 = (i ** 2 for i in CountDown(3)) # immediately prints "ITER" print("Go!") for x in g1: print(x)
mientras:
def my_generator(): for i in CountDown(3): yield i ** 2 g2 = my_generator() print("Go!") for x in g2: # "ITER" is only printed here print(x)
Dado que la mayoría de los iteradores no hacen muchas cosas
__iter__
, es fácil pasar por alto este comportamiento. Un ejemplo del mundo real sería el de DjangoQuerySet
, que captura datos__iter__
ydata = (f(x) for x in qs)
puede llevar mucho tiempo, mientras quedef g(): for x in qs: yield f(x)
seguido dedata=g()
volvería inmediatamente.Para obtener más información y la definición formal, consulte PEP 289 - Expresiones generadoras .
fuente
Existe una diferencia que podría ser importante en algunos contextos que aún no se ha señalado. El uso le
yield
impide usarreturn
para algo más que generar implícitamente StopIteration (y cosas relacionadas con las corrutinas) .Esto significa que este código está mal formado (y dárselo a un intérprete le dará una
AttributeError
):class Tea: """With a cloud of milk, please""" def __init__(self, temperature): self.temperature = temperature def mary_poppins_purse(tea_time=False): """I would like to make one thing clear: I never explain anything.""" if tea_time: return Tea(355) else: for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']: yield item print(mary_poppins_purse(True).temperature)
Por otro lado, este código funciona como un encanto:
class Tea: """With a cloud of milk, please""" def __init__(self, temperature): self.temperature = temperature def mary_poppins_purse(tea_time=False): """I would like to make one thing clear: I never explain anything.""" if tea_time: return Tea(355) else: return (item for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']) print(mary_poppins_purse(True).temperature)
fuente