Entendiendo los generadores en Python

Estoy leyendo el libro de cocina de Python en este momento y actualmente estoy mirando los generadores. Me está resultando difícil conseguir mi cabeza redonda.

Como vengo de un fondo de Java, ¿hay un equivalente de Java? El libro hablaba de ‘Productor / Consumidor’, pero 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 simple y decente directamente de un libro). Tal vez con ejemplos, si te sientes generoso!

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

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

Las funciones normales devuelven un solo valor utilizando return , al igual que en Java. En Python, sin embargo, hay una alternativa, llamada yield . Usar el yield en cualquier parte de una función lo convierte en un generador. Observe 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 "", line 1, in  StopIteration 

Como puede ver, myGen(n) es una función que produce n y n + 1 . Cada llamada a la next produce un solo valor, hasta que todos los valores hayan sido cedidos. for bucles llamar a next en el 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 de manera sucinta 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 "", line 1, in  StopIteration 

Tenga en cuenta que las expresiones generadoras se parecen mucho a las comprensiones de lista :

 >>> 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 a la vez. Solo las llamadas al next ejecutan (parte de) el código. La ejecución del código en un generador se detiene una vez que se ha alcanzado una statement de yield , sobre la cual devuelve un valor. La siguiente llamada a la next hace que la ejecución continúe en el estado en que se dejó el generador después del último yield . Esta es una diferencia fundamental con las funciones regulares: estas 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 send datos de vuelta a un generador ( referencia ). Pero eso es algo que sugiero que no analice hasta que entienda el concepto básico de un generador.

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

  • Ciertos conceptos se pueden describir mucho más sucintamente 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 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.islice para tomar un número finito de elementos de un flujo infinito. Se recomienda tener una buena itertools las funciones en el módulo de itertools , ya que son herramientas esenciales para escribir generadores avanzados con gran facilidad.


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

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

Un generador es efectivamente una función que devuelve (datos) antes de que finalice, 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 (o uno) beneficio de los generadores es que debido a que tratan con datos de una pieza a la vez, puede manejar grandes cantidades de datos; con las listas, los requisitos de memoria excesivos podrían convertirse en un problema. Los generadores, al igual que las listas, son iterables, por lo que pueden usarse 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 obtiene cada respuesta cada vez que lo solicita.

En primer lugar, el término generador originalmente estaba algo mal definido en Python, lo que generó 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 a un objeto generador).

De acuerdo con 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 corregido.

Puede ser una buena idea seguir siendo precisos y evitar el término “generador” sin más especificaciones.

Los generadores pueden considerarse como una abreviatura para crear un iterador. Se comportan como un iterador de Java. Ejemplo:

 >>> g = (x for x in range(10)) >>> g  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 "", line 1, in  StopIteration 

Espero que esto ayude / es lo que buscas.

Actualizar:

Como muchas otras respuestas muestran, hay diferentes formas de crear un generador. Puede usar la syntax de paréntesis como en mi ejemplo anterior, o puede usar el rendimiento. Otra característica interesante es que los generadores pueden ser “infinitos”: los 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 ... 

No hay equivalente en Java.

Aquí está un poco de un 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 se ejecuta de 0 a n, y si la variable de bucle es un múltiplo de 3, produce la variable.

Durante cada iteración del bucle for 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.

Me gusta describir los generadores, a aquellos con un fondo decente en lenguajes de progtwigción y computación, en términos de marcos de stack.

En muchos idiomas, hay una stack encima de la cual está el “marco” de la stack actual. El marco de stack 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 progtwig” o su equivalente) se inserta en la stack y se crea un nuevo marco de stack. La ejecución luego se transfiere al principio de la función que se está llamando.

Con las funciones normales, en algún momento la función devuelve un valor y la stack se “abre”. El marco de stack 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 el marco de stack se descarte, utilizando la statement de rendimiento. Los valores de las variables locales y el contador del progtwig dentro de la función se conservan. Esto permite que el generador se reanude en un momento posterior, con la ejecución continuada desde la statement de rendimiento, y puede ejecutar más código y devolver otro valor.

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

La ventaja clave para los generadores es que el “estado” de la función se conserva, a diferencia de las funciones regulares donde cada vez que se descarta el marco de la stack, se pierde todo ese “estado”. Una ventaja secundaria es que se evita parte de la sobrecarga de llamada a la función (crear y eliminar marcos de stack), aunque esta es una ventaja menor.

Lo único que puedo agregar a la respuesta de Stephan202 es una recomendación para que eches un vistazo a la presentación PyCon ’08 de David Beazley “Trucos del generador para progtwigdores de sistemas”, 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 algo divertido” a “Esto es lo que he estado buscando”. Está en http://www.dabeaz.com/generators/ .

Es útil 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 se imprime

 # 6 # 7 

Piense en un generador como una función reanudable.

yield comporta como return en el sentido de que los valores que se obtienen son “devueltos” por el generador. Sin embargo, a diferencia de la devolución, la próxima vez que se le solicite al generador un valor, la función del generador, foo, se reanuda donde se quedó, después de la última statement de rendimiento, y continúa ejecutándose hasta que llega a otra statement de rendimiento.

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

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 foo termina (y no hay más valores de rendimiento), al llamar a next(bar) produce un error StopInteration.

Esta publicación utilizará los números de Fibonacci como una herramienta para desarrollar la explicación de la utilidad de los generadores 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 de C ++ de forma extremadamente sencilla:

 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, estará recalculando muchos de los valores con la función anterior.

Por ejemplo: Fib(3) = Fib(2) + Fib(1) , pero Fib(2) también vuelve a calcular Fib(1) . Cuanto más alto sea el valor que desea calcular, peor será el valor.

Por lo tanto, uno puede tener la tentación de volver a escribir lo anterior al hacer 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 en main . Sería mejor no tener que preocuparse por el estado en nuestra función main .

Podríamos devolver un vector de valores y usar un iterator para iterar sobre ese conjunto de valores, pero esto requiere mucha memoria a la vez para una gran cantidad de valores de retorno.

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

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

 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 main no está tan desordenada, y puede hacer lo que quiera con los números de Fibonacci, simplemente defina nuevas devoluciones de llamada.

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

Bueno, podríamos seguir como hemos estado, y podríamos comenzar a agregar el estado nuevamente a main , permitiendo que GetFibNumbers comience desde un punto arbitrario. Pero esto boostá 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 su lugar vamos a hablar 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 devolviendo 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 statement de yield se utiliza junto con los generadores de Python. Guarda el estado de la función y devuelve el valor establecido. La próxima vez que llame a la función next () en el generador, continuará donde el rendimiento se detuvo.

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

Fuente

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

Puede disfrutar de la descripción general de Icon , que le permite envolver su cabeza alrededor de ellos sin concentrarse en la syntax (ya que Icon es un idioma que probablemente no conozca, y Griswold estaba explicando los beneficios de su idioma a personas que vienen de otros idiomas).

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

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

Por ejemplo, el siguiente código de sum creará una lista completa de cuadrados en la memoria, iterará sobre esos valores y, cuando la referencia ya no sea necesaria, elimine 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 de objetos contenedores:

 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