Pregunta Python: expresión del generador vs. rendimiento


En Python, ¿hay alguna diferencia entre crear un objeto generador a través de expresión del generador versus usar el rendimiento ¿declaración?

Utilizando rendimiento:

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

Utilizando 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, p. (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!


77
2018-01-03 16:09


origen


Respuestas:


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

Editar: Mi primera versión descompila la expresión del generador creada en module-scope en el prompt interactivo. Eso es ligeramente diferente de la versión del OP con el que se usa dentro de una función. Modifiqué 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 principio FOR_ITER difieren en un solo aspecto: el enfoque de "rendimiento" usa una LOAD_FAST en lugar de un LOAD_DEREF dentro del bucle los LOAD_DEREF es "bastante lento" que LOAD_FAST, por lo que hace que la versión de "rendimiento" sea ligeramente 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 envolverlo 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 ligeramente más rápida para valores de bucle más pequeños, incluso si LOAD_FAST dio a la versión de "rendimiento" una ventaja de lo contrario.

En ninguno de los casos, la diferencia de rendimiento sería suficiente para justificar la decisión entre uno u otro. La legibilidad cuenta mucho más, por lo tanto, use la que sea 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

68
2018-01-03 17:17



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. Leer PEP 342 para más detalles (es una técnica interesante que vale la pena conocer).

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

PD Aquí hay un ejemplo simple de corutina 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!")

35
2018-01-03 16:13



No hay diferencia para el tipo de bucles simples que puede caber en una expresión de generador. Sin embargo, el rendimiento puede usarse para crear generadores que realizan 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]

17
2018-01-03 16:30



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

8
2018-01-03 16:13



En el uso, tenga en cuenta una distinción entre un objeto generador vs una función del generador.

Un objeto generador se usa solo una vez, en contraste con una función del generador, que se puede reutilizar cada vez que se vuelve a llamar, porque devuelve un objeto generador nuevo.

Las expresiones de generador se utilizan en la práctica como "en bruto", sin envolverlas en una función, y devuelven un objeto de 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()))

qué 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))

qué salidas:

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

Y compare con una expresión de generador:

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

8
2018-01-12 06:26



Al pensar en iteradores, el itertools mó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 iterativa" que permite construir herramientas especializadas de manera sucinta y eficiente en Python puro.

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

Producto cartesiano de los iterables de entrada.

Equivalente a los for-loops anidados en una expresión de generador. 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)]
>>> 

5
2018-01-03 16:21



Sí, hay una diferencia.

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

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

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

g = my_generator()

iter(expr) todavía no se llama. Se llamará solo al iterar en g (y podría no ser llamado en absoluto).

Tomando este iterador como un 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 en __iter__, es fácil pasar por alto este comportamiento. Un ejemplo del mundo real sería el de Django QuerySet, cual buscar datos en __iter__ y data = (f(x) for x in qs) puede tomar mucho tiempo, mientras def g(): for x in qs: yield f(x) seguido por data=g() regresaría de inmediato.

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


2
2018-05-17 23:01



Existe una diferencia que podría ser importante en algunos contextos que aún no se ha señalado. Utilizando yield le impide usar return por algo más que criar implícitamente StopIteration (y cosas relacionadas con corutinas).

Esto significa que este código está mal formado (y alimentarlo 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)

0
2017-11-17 00:07