Python: expresión generadora vs. rendimiento

En Python, ¿hay alguna diferencia entre crear un objeto generador a través de una expresión generadora y usar la statement de rendimiento ?

Utilizando el rendimiento :

def Generator(x, y): for i in xrange(x): for j in xrange(y): yield(i, j) 

Usando la expresión del generador :

 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!

Sólo hay pequeñas diferencias en los dos. Puedes usar el módulo dis para examinar este tipo de cosas por ti mismo.

Edición: mi primera versión descompiló la expresión generadora creada en module-scope en el indicador interactivo. Eso es un poco diferente de la versión del OP que se usa dentro de una función. He modificado esto para que coincida con el caso real en 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 primer FOR_ITER se diferencian en un solo aspecto: el enfoque de “rendimiento” usa un LOAD_FAST en lugar de un LOAD_DEREF dentro del lazo. LOAD_DEREF es “bastante más lento” que LOAD_FAST , por lo que hace que la versión de “rendimiento” sea un poco más rápida que la expresión del generador para valores suficientemente grandes de x (el bucle externo) porque el valor de y se carga un poco más rápido en cada pasada. Para valores más pequeños de x 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 normalmente 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 de 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 a la versión de “rendimiento” una ventaja de lo contrario.

En ninguno de los dos casos, la diferencia de rendimiento sería suficiente para justificar la decisión entre uno u otro. La legibilidad es mucho más importante, 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 

En este ejemplo, no realmente. Pero el yield se puede utilizar para construcciones más complejas, por ejemplo , puede aceptar valores de la persona que llama y modificar el flujo como resultado. Lee PEP 342 para más detalles (es una técnica interesante que vale la pena conocer).

De todos modos, el mejor consejo es usar lo que sea más claro para sus necesidades .

PS Aquí hay un ejemplo 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!") 

No hay diferencia para el tipo de bucles simples que puede encajar en una expresión de generador. Sin embargo, el rendimiento se puede utilizar para crear generadores que hacen un procesamiento mucho más complejo. Aquí hay un ejemplo simple para generar la secuencia de fibonacci:

 >>> def fibgen(): ... a = b = 1 ... while 1: ... 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] 

En uso, observe una distinción entre un objeto generador frente a una función de generador.

Un objeto generador se usa solo una vez, a diferencia de la función de un generador, que se puede reutilizar cada vez que se vuelve a llamar, porque devuelve un objeto generador nuevo.

En la práctica, las expresiones de los generadores generalmente se usan “en bruto”, 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 produce:

 [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] 

Comparar con un uso ligeramente diferente:

 range_10_gen = range_10_gen_func() print(list(range_10_gen)) print(list(range_10_gen)) print(list(range_10_gen)) 

que produce:

 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [] [] 

Y comparar 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 produce:

 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [] [] 

Usar el yield es bueno si la expresión es más complicada que los bucles nesteds. Entre otras cosas, puede devolver un primer valor especial o un valor final especial. Considerar:

 def Generator(x): for i in xrange(x): yield(i) yield(None) 

Al pensar en iteradores, el módulo de itertools :

… estandariza un conjunto básico de herramientas rápidas y eficientes en memoria que son útiles por sí mismas o en combinación. Juntos, forman un “álgebra de iteradores” que hace posible construir herramientas especializadas de forma sucinta y eficiente en Python puro.

Para el rendimiento, considere itertools.product(*iterables[, repeat])

Producto cartesiano de entrada iterables.

Equivalente a bucles for nesteds en una expresión generadora. Por ejemplo, el product(A, B) devuelve lo mismo que ((x,y) for x in A for y in B) .

 >>> 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)] >>> 

Sí, hay una diferencia.

Para la expresión del generador (x for var in expr) , se llama a iter(expr) cuando se crea la expresión.

Cuando se usa def y yield para crear un generador, como en:

 def my_generator(): for var in expr: yield x g = my_generator() 

iter(expr) aún no se llama. Se llamará solo cuando se repita en g (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) 

Como la mayoría de los iteradores no hacen muchas cosas en __iter__ , es fácil pasar por alto este comportamiento. Un ejemplo del mundo real sería el QuerySet de Django, que recuperar datos en __iter__ y data = (f(x) for x in qs) puede llevar mucho tiempo, mientras que def g(): for x in qs: yield f(x) seguido de data=g() regresaría inmediatamente.

Para obtener más información y la definición formal, consulte PEP 289 – Generator Expressions .

Hay una diferencia que podría ser importante en algunos contextos que aún no se ha señalado. El uso del yield impide usar el return para otra cosa que el aumento implícito de StopIteration (y cosas relacionadas con las rutinas) .

Esto significa que este código está mal formado (y si se lo envía a un intérprete le dará un 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 a la perfección:

 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)