Python: expresión generadora frente a rendimiento

90

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!

cschol
fuente
2
Elija el que le resulte más legible.
user238424

Respuestas:

74

Solo hay ligeras diferencias entre los dos. Puede utilizar el dismó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_ITERse diferencian sólo en un aspecto: el enfoque de "rendimiento" usa a LOAD_FASTen lugar de a LOAD_DEREFdentro del ciclo. El LOAD_DEREFes "bastante más lento" que LOAD_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 de x(el ciclo externo) porque el valor de yse carga un poco más rápido en cada pasada. Para valores más pequeños 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 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_FASTle 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
Peter Hansen
fuente
Aceptado: para obtener una explicación detallada de la diferencia con dis. ¡Gracias!
cschol
Actualicé para incluir un enlace a una fuente que afirma que LOAD_DEREFes "bastante más lenta", por lo que si el rendimiento realmente importara timeit, sería bueno el momento real . Un análisis teórico sólo llega hasta cierto punto.
Peter Hansen
36

En este ejemplo, no realmente. Pero yieldse 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!")
Eli Bendersky
fuente
8
+1 por vincular a David Beazley. Su presentación sobre corrutinas es lo más alucinante que he leído en mucho tiempo. No tan útil, tal vez, como su presentación sobre generadores, pero asombroso de todos modos.
Robert Rossney
18

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]
Dave Kirby
fuente
5
+1 que es súper genial ... no puedo decir que haya visto una implementación de fib tan corta y dulce sin recursividad.
JudoWill
Fragmento de código engañosamente simple: ¡creo que Fibonacci estará feliz de verlo!
user-asterix
10

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:

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

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]
[]
[]
Craig McQueen
fuente
8

Usar yieldes 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)
Tor Valamo
fuente
5

Al pensar en iteradores, el itertoolsmódulo:

... 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 manera sucinta y eficiente en Python puro.

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

Producto cartesiano de iterables de entrada.

Equivalente a bucles for anidados en una expresión generadora. Por ejemplo, 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)]
>>> 
gimel
fuente
4

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 defy yieldpara 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 iterar 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)

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 Django QuerySet, que captura datos__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()volvería inmediatamente.

Para obtener más información y la definición formal, consulte PEP 289 - Expresiones generadoras .

Udi
fuente
0

Existe una diferencia que podría ser importante en algunos contextos que aún no se ha señalado. El uso le yieldimpide usar returnpara 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)
Adrien
fuente