Comprender los generadores en Python


218

Estoy leyendo el libro de cocina de Python en este momento y actualmente estoy mirando generadores. Me resulta difícil entenderlo.

Como vengo de un fondo de Java, ¿hay un equivalente de Java? El libro hablaba de 'Productor / Consumidor', sin embargo, cuando escucho eso, pienso en enhebrar.

¿Qué es un generador y por qué lo usarías? Sin citar ningún libro, obviamente (a menos que pueda encontrar una respuesta decente y simplista directamente de un libro). ¡Quizás con ejemplos, si te sientes generoso!

Respuestas:


402

Nota: esta publicación asume la sintaxis de Python 3.x.

Un generador es simplemente una función que devuelve un objeto al que puede llamar next, de modo que por cada llamada devuelve algún valor, hasta que genera una StopIterationexcepción, lo que indica que se han generado todos los valores. Tal objeto se llama iterador .

Las funciones normales devuelven un solo valor utilizando return, al igual que en Java. En Python, sin embargo, hay una alternativa, llamada yield. El uso yielden cualquier lugar de una función lo convierte en un generador. Observa este código:

>>> def myGen(n):
...     yield n
...     yield n + 1
... 
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Como puede ver, myGen(n)es una función que produce ny n + 1. Cada llamada a nextproduce un valor único, hasta que se hayan producido todos los valores. forlos bucles llaman nextal fondo, por lo tanto:

>>> for n in myGen(6):
...     print(n)
... 
6
7

Del mismo modo, hay expresiones generadoras , que proporcionan un medio para describir sucintamente ciertos tipos comunes de generadores:

>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Tenga en cuenta que las expresiones generadoras son muy parecidas a las comprensiones de listas :

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

Observe que un objeto generador se genera una vez , pero su código no se ejecuta de una vez. Solo llamadas para nextejecutar realmente (parte de) el código. La ejecución del código en un generador se detiene una vez yieldque se ha alcanzado una declaración, sobre la cual devuelve un valor. La siguiente llamada a nexthace que la ejecución continúe en el estado en el que quedó el generador después de la última yield. Esta es una diferencia fundamental con las funciones regulares: esas siempre comienzan la ejecución en la "parte superior" y descartan su estado al devolver un valor.

Hay más cosas que decir sobre este tema. Por ejemplo, es posible sendvolver a ingresar los datos en un generador ( referencia ). Pero eso es algo que le sugiero que no examine hasta que comprenda el concepto básico de un generador.

Ahora puede preguntar: ¿por qué usar generadores? Hay un par de buenas razones:

  • Ciertos conceptos pueden describirse de manera mucho más sucinta usando generadores.
  • En lugar de crear una función que devuelva una lista de valores, se puede escribir un generador que genere los valores sobre la marcha. Esto significa que no es necesario construir una lista, lo que significa que el código resultante es más eficiente en la memoria. De esta manera, incluso se pueden describir flujos de datos que simplemente serían demasiado grandes para caber en la memoria.
  • Los generadores permiten una forma natural de describir flujos infinitos . Considere, por ejemplo, los números de Fibonacci :

    >>> def fib():
    ...     a, b = 0, 1
    ...     while True:
    ...         yield a
    ...         a, b = b, a + b
    ... 
    >>> import itertools
    >>> list(itertools.islice(fib(), 10))
    [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    

    Este código usa itertools.islicepara tomar un número finito de elementos de una secuencia infinita. Se recomienda observar bien las funciones del itertoolsmódulo, ya que son herramientas esenciales para escribir generadores avanzados con gran facilidad.


   Acerca de Python <= 2.6: en los ejemplos anteriores nexthay una función que llama al método __next__en el objeto dado. En Python <= 2.6 se usa una técnica ligeramente diferente, en o.next()lugar de next(o). Python 2.7 tiene next()llamada, .nextpor lo que no necesita usar lo siguiente en 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3

99
Usted menciona que es posible enviar senddatos a un generador. Una vez que haces eso, tienes una 'corutina'. Es muy simple implementar patrones como el mencionado consumidor / productor con corutinas porque no tienen necesidad de Locks y, por lo tanto, no pueden llegar a un punto muerto. Es difícil describir las corutinas sin atacar los hilos, por lo que solo diré que las corutinas son una alternativa muy elegante a los hilos.
Jochen Ritzel

¿Los generadores de Python son básicamente máquinas de Turing en términos de cómo funcionan?
Fiery Phoenix

48

Un generador es efectivamente una función que devuelve (datos) antes de que termine, pero se detiene en ese punto, y puede reanudar la función en ese punto.

>>> def myGenerator():
...     yield 'These'
...     yield 'words'
...     yield 'come'
...     yield 'one'
...     yield 'at'
...     yield 'a'
...     yield 'time'

>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words

y así. El beneficio (o uno) de los generadores es que debido a que manejan datos de una pieza a la vez, puede manejar grandes cantidades de datos; con listas, los requisitos de memoria excesiva podrían convertirse en un problema. Los generadores, al igual que las listas, son iterables, por lo que se pueden usar de la misma manera:

>>> for word in myGeneratorInstance:
...     print word
These
words
come
one
at 
a 
time

Tenga en cuenta que los generadores proporcionan otra forma de lidiar con el infinito, por ejemplo

>>> from time import gmtime, strftime
>>> def myGen():
...     while True:
...         yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())    
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000   

El generador encapsula un bucle infinito, pero esto no es un problema porque solo obtienes cada respuesta cada vez que la solicitas.


30

En primer lugar, el término generador originalmente estaba algo mal definido en Python, lo que genera mucha confusión. Probablemente te refieres a iteradores e iterables (ver aquí ). Luego, en Python también hay funciones generadoras (que devuelven un objeto generador), objetos generadores (que son iteradores) y expresiones generadoras (que se evalúan como un objeto generador).

Según la entrada del glosario para el generador , parece que la terminología oficial es ahora que el generador es la abreviatura de "función del generador". En el pasado, la documentación definía los términos de manera inconsistente, pero afortunadamente esto se ha solucionado.

Todavía podría ser una buena idea ser preciso y evitar el término "generador" sin más especificaciones.


2
Hmm, creo que tienes razón, al menos según una prueba de algunas líneas en Python 2.6. Una expresión de generador devuelve un iterador (también conocido como 'objeto generador'), no un generador.
Craig McQueen, el

22

Se podría considerar que los generadores son una forma abreviada de crear un iterador. Se comportan como un iterador de Java. Ejemplo:

>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g)   # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next()  # iterator is at the end; calling next again will throw
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Espero que esto ayude / es lo que estás buscando.

Actualizar:

Como se muestran muchas otras respuestas, hay diferentes formas de crear un generador. Puede usar la sintaxis de paréntesis como en mi ejemplo anterior, o puede usar el rendimiento. Otra característica interesante es que los generadores pueden ser "infinitos", iteradores que no se detienen:

>>> def infinite_gen():
...     n = 0
...     while True:
...         yield n
...         n = n + 1
... 
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...

1
Ahora, Java tiene Streams, que son mucho más similares a los generadores, excepto que aparentemente no puede obtener el siguiente elemento sin una cantidad sorprendente de problemas.
Fondo de la demanda de Mónica

12

No hay equivalente de Java.

Aquí hay un pequeño ejemplo artificial:

#! /usr/bin/python
def  mygen(n):
    x = 0
    while x < n:
        x = x + 1
        if x % 3 == 0:
            yield x

for a in mygen(100):
    print a

Hay un bucle en el generador que va de 0 a n, y si la variable del bucle es un múltiplo de 3, produce la variable.

Durante cada iteración del forbucle se ejecuta el generador. Si es la primera vez que se ejecuta el generador, comienza desde el principio; de lo contrario, continúa desde el momento anterior en que se produjo.


2
El último párrafo es muy importante: el estado de la función del generador se 'congela' cada vez que produce algo, y continúa exactamente en el mismo estado cuando se invoca la próxima vez.
Johannes Charra

No existe un equivalente sintáctico en Java para una "expresión generadora", pero los generadores, una vez que tiene uno, son esencialmente solo un iterador (las mismas características básicas que un iterador Java).
pensar demasiado el

@overthink: Bueno, los generadores pueden tener otros efectos secundarios que los iteradores de Java no pueden tener. Si tuviera que poner print "hello"después de x=x+1en mi ejemplo, "hola" se imprimiría 100 veces, mientras que el cuerpo del bucle for solo se ejecutaría 33 veces.
Wernsey

@iWerner: Estoy bastante seguro de que se podría tener el mismo efecto en Java. La implementación de next () en el iterador Java equivalente aún tendría que buscar de 0 a 99 (usando su ejemplo mygen (100)), por lo que podría System.out.println () cada vez si lo desea. Sin embargo, solo regresarías 33 veces desde next (). Lo que le falta a Java es la sintaxis de rendimiento muy útil, que es significativamente más fácil de leer (y escribir).
pensar demasiado el

Me encantó leer y recordar esta definición de una línea: si es la primera vez que se ejecuta el generador, comienza desde el principio; de lo contrario, continúa desde el momento anterior.
Iqra

8

Me gusta describir los generadores, a aquellos con una buena formación en lenguajes de programación y computación, en términos de marcos de pila.

En muchos idiomas, hay una pila sobre la cual se encuentra el "marco" de la pila actual. El marco de la pila incluye el espacio asignado para las variables locales a la función, incluidos los argumentos pasados ​​a esa función.

Cuando llama a una función, el punto de ejecución actual (el "contador de programa" o equivalente) se inserta en la pila y se crea un nuevo marco de pila. La ejecución luego se transfiere al comienzo de la función que se llama.

Con funciones regulares, en algún momento la función devuelve un valor, y la pila se "abre". El marco de la pila de la función se descarta y la ejecución se reanuda en la ubicación anterior.

Cuando una función es un generador, puede devolver un valor sin que se descarte el marco de la pila, utilizando la declaración de rendimiento. Se conservan los valores de las variables locales y el contador del programa dentro de la función. Esto permite que el generador se reanude más adelante, con la ejecución continua desde la declaración de rendimiento, y puede ejecutar más código y devolver otro valor.

Antes de Python 2.5 esto era todo lo que hicieron los generadores. Python 2.5 añade la capacidad de pasar valores de nuevo en al generador también. Al hacerlo, el valor pasado está disponible como una expresión resultante de la declaración de rendimiento que había devuelto temporalmente el control (y un valor) del generador.

La ventaja clave para los generadores es que se preserva el "estado" de la función, a diferencia de las funciones regulares donde cada vez que se descarta el marco de la pila, se pierde todo ese "estado". Una ventaja secundaria es que se evita parte de la sobrecarga de llamadas a funciones (creación y eliminación de marcos de pila), aunque esta suele ser una ventaja menor.


6

Lo único que puedo agregar a la respuesta de Stephan202 es una recomendación de que eche un vistazo a la presentación de David Beazley PyCon '08 "Generator Tricks for Systems Programmers", que es la mejor explicación del cómo y por qué de los generadores que he visto. en cualquier sitio. Esto es lo que me llevó de "Python parece divertido" a "Esto es lo que he estado buscando". Está en http://www.dabeaz.com/generators/ .


6

Ayuda a hacer una distinción clara entre la función foo y el generador foo (n):

def foo(n):
    yield n
    yield n+1

Foo es una función. foo (6) es un objeto generador.

La forma típica de usar un objeto generador es en un bucle:

for n in foo(6):
    print(n)

El bucle imprime

# 6
# 7

Piense en un generador como una función reanudable.

yieldse comporta como returnen el sentido de que los valores que se obtienen son "devueltos" por el generador. Sin embargo, a diferencia del retorno, la próxima vez que se le solicite un valor al generador, la función del generador, foo, se reanuda donde se quedó, después de la última declaración de rendimiento, y continúa ejecutándose hasta que alcanza otra declaración de rendimiento.

Detrás de escena, cuando llama a bar=foo(6)la barra de objetos del generador, se define para que tenga un nextatributo.

Puede llamarlo usted mismo para recuperar los valores obtenidos de foo:

next(bar)    # Works in Python 2.6 or Python 3.x
bar.next()   # Works in Python 2.5+, but is deprecated. Use next() if possible.

Cuando termina foo (y no hay más valores cedidos), la llamada next(bar)arroja un error StopInteration.


5

Esta publicación utilizará los números de Fibonacci como una herramienta para explicar la utilidad de los generadores de Python .

Esta publicación contará con código C ++ y Python.

Los números de Fibonacci se definen como la secuencia: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

O en general:

F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2

Esto se puede transferir a una función C ++ extremadamente fácil:

size_t Fib(size_t n)
{
    //Fib(0) = 0
    if(n == 0)
        return 0;

    //Fib(1) = 1
    if(n == 1)
        return 1;

    //Fib(N) = Fib(N-2) + Fib(N-1)
    return Fib(n-2) + Fib(n-1);
}

Pero si desea imprimir los primeros seis números de Fibonacci, volverá a calcular muchos de los valores con la función anterior.

Por ejemplo:, Fib(3) = Fib(2) + Fib(1)pero Fib(2)también recalcula Fib(1). Cuanto mayor sea el valor que desea calcular, peor será.

Por lo tanto, uno puede verse tentado a reescribir lo anterior haciendo un seguimiento del estado main.

// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
    int result = pp + p;
    pp = p;
    p = result;
    return result;
}

int main(int argc, char *argv[])
{
    size_t pp = 0;
    size_t p = 1;
    std::cout << "0 " << "1 ";
    for(size_t i = 0; i <= 4; ++i)
    {
        size_t fibI = GetNextFib(pp, p);
        std::cout << fibI << " ";
    }
    return 0;
}

Pero esto es muy feo y complica nuestra lógica main. Sería mejor no tener que preocuparse por el estado en nuestra mainfunción.

Podríamos devolver un vectorvalor y usar un iteratorpara iterar sobre ese conjunto de valores, pero esto requiere mucha memoria de una vez para una gran cantidad de valores devueltos.

Volviendo a nuestro antiguo enfoque, ¿qué sucede si quisiéramos hacer otra cosa además de imprimir los números? Tendríamos que copiar y pegar todo el bloque de código mainy cambiar las declaraciones de salida a cualquier otra cosa que quisiéramos hacer. Y si copia y pega el código, entonces debería recibir un disparo. No quieres que te disparen, ¿verdad?

Para resolver estos problemas y evitar que te disparen, podemos reescribir este bloque de código usando una función de devolución de llamada. Cada vez que se encuentra un nuevo número de Fibonacci, llamamos a la función de devolución de llamada.

void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
    if(max-- == 0) return;
    FoundNewFibCallback(0);
    if(max-- == 0) return;
    FoundNewFibCallback(1);

    size_t pp = 0;
    size_t p = 1;
    for(;;)
    {
        if(max-- == 0) return;
        int result = pp + p;
        pp = p;
        p = result;
        FoundNewFibCallback(result);
    }
}

void foundNewFib(size_t fibI)
{
    std::cout << fibI << " ";
}

int main(int argc, char *argv[])
{
    GetFibNumbers(6, foundNewFib);
    return 0;
}

Esto es claramente una mejora, su lógica mainno está tan abarrotada y puede hacer lo que quiera con los números de Fibonacci, simplemente defina nuevas devoluciones de llamadas.

Pero esto aún no es perfecto. ¿Qué sucede si solo desea obtener los dos primeros números de Fibonacci y luego hacer algo, luego obtener algo más y luego hacer otra cosa?

Bueno, podríamos continuar como lo hemos sido, y podríamos comenzar a agregar estado nuevamente main, permitiendo que GetFibNumbers comience desde un punto arbitrario. Pero esto aumentará aún más nuestro código, y ya parece demasiado grande para una tarea simple como imprimir números de Fibonacci.

Podríamos implementar un modelo de productor y consumidor a través de un par de hilos. Pero esto complica aún más el código.

En cambio hablemos de generadores.

Python tiene una característica de lenguaje muy agradable que resuelve problemas como estos llamados generadores.

Un generador le permite ejecutar una función, detenerse en un punto arbitrario y luego continuar nuevamente donde lo dejó. Cada vez que devuelve un valor.

Considere el siguiente código que usa un generador:

def fib():
    pp, p = 0, 1
    while 1:
        yield pp
        pp, p = p, pp+p

g = fib()
for i in range(6):
    g.next()

Lo que nos da los resultados:

0 1 1 2 3 5

La yielddeclaración se usa en conjunción con generadores Python. Guarda el estado de la función y devuelve el valor final. La próxima vez que llame a la función next () en el generador, continuará donde se quedó el rendimiento.

Esto es mucho más limpio que el código de función de devolución de llamada. Tenemos un código más limpio, un código más pequeño y sin mencionar mucho más código funcional (Python permite enteros arbitrariamente grandes).

Fuente


3

Creo que la primera aparición de iteradores y generadores fue en el lenguaje de programación Icon, hace unos 20 años.

Puede disfrutar de la descripción general de Icon , que le permite entenderlo sin concentrarse en la sintaxis (dado que Icon es un idioma que probablemente no conoce, y Griswold estaba explicando los beneficios de su idioma a las personas que vienen de otros idiomas).

Después de leer solo unos pocos párrafos allí, la utilidad de los generadores e iteradores podría ser más evidente.


2

La experiencia con la comprensión de listas ha demostrado su utilidad generalizada en Python. Sin embargo, muchos de los casos de uso no necesitan tener una lista completa creada en la memoria. En cambio, solo necesitan iterar sobre los elementos uno a la vez.

Por ejemplo, el siguiente código de suma creará una lista completa de cuadrados en la memoria, iterará sobre esos valores y, cuando la referencia ya no sea necesaria, eliminará la lista:

sum([x*x for x in range(10)])

La memoria se conserva utilizando una expresión generadora en su lugar:

sum(x*x for x in range(10))

Se otorgan beneficios similares a los constructores para objetos de contenedor:

s = Set(word  for line in page  for word in line.split())
d = dict( (k, func(k)) for k in keylist)

Las expresiones generadoras son especialmente útiles con funciones como sum (), min () y max () que reducen una entrada iterable a un solo valor:

max(len(line)  for line in file  if line.strip())

más


1

Puse este código que explica 3 conceptos clave sobre generadores:

def numbers():
    for i in range(10):
            yield i

gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers

for i in gen: #we iterate over the generator and the values are printed
    print(i)

#the generator is now empty

for i in gen: #so this for block does not print anything
    print(i)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.