Comprender los generadores en Python


Estoy leyendo el libro de cocina de Python en este momento y actualmente estoy mirando generadores. Me está costando entender.

Como vengo de un fondo Java, ¿hay un equivalente Java? El libro hablaba de "Productor / Consumidor", sin embargo, cuando oigo que 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). Tal vez con ejemplos, si te sientes generoso!

Author: Peter Mortensen, 2009-11-18

11 answers

Nota: este post asume Python 3.x sintaxis.

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

Las funciones normales devuelven un solo valor usando return, al igual que en Java. En Python, sin embargo, hay una alternativa, llamado yield. Usar yield en cualquier lugar de una función la 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 "<stdin>", line 1, in <module>
StopIteration

Como puedes ver, myGen(n) es una función que produce n y n + 1. Cada llamada a next produce un solo valor, hasta que todos los valores se han producido. for los bucles llaman next en segundo plano, así:

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

Asimismo hay expresiones del generador, 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 del generador son muy similares comprensiones de la 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 es no se ejecuta todo a la vez. Solo las llamadas a next realmente ejecutan (parte de) el código. La ejecución del código en un generador se detiene una vez que se ha alcanzado una instrucción yield, sobre la cual devuelve un valor. La siguiente llamada a next hace que la ejecución continúe en el estado en el que el generador se dejó después de la última yield. Esta es una diferencia fundamental con las funciones regulares: aquellas 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 que sendlos datos vuelvan a un generador ( referencia). Pero eso es algo que sugiero que no mires hasta que entiendas el concepto básico de un generador.

Ahora puede preguntar: ¿por qué usar los 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 devuelve una lista de valores, se puede escribir un generador que genere los valores sobre la marcha. Esto significa que no es necesario construir ninguna lista, lo que significa que el código resultante es más eficiente en memoria. De esta manera uno puede incluso describir flujos de datos que simplemente serían demasiado grandes para encajar memoria.
  • Los generadores permiten una forma natural de describir flujos infinitos. Consideremos 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 utiliza itertools.islice tomar un número finito de elementos de una corriente infinita. Se recomienda tener un buen vistazo a las funciones en el itertools módulo, ya que son herramientas esenciales para escribir generadores avanzados con gran facilidad.


Acerca de Python en los ejemplos anteriores next es una función que llama al método __next__ en el objeto dado. En Python o.next() en lugar de next(o). Python 2.7 tiene next() llamada .next por lo que no es necesario utilizar lo siguiente en 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3
 323
Author: Stephan202,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-07 02:33:27

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í sucesivamente. El (o uno) beneficio de los generadores es que debido a que tratan con datos de una pieza a la vez, puede tratar con 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 se pueden usar de la misma manera:

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

Nota 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 pides.

 41
Author: Caleb Hattingh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-26 14:21:02

En primer lugar, el término generator originalmente estaba algo mal definido en Python, lo que llevó a mucha confusión. Usted probablemente significa iteradores y iterables (ver aquí). Luego, en Python también hay funciones generadoras (que devuelven un objeto generator), objetos generator (que son iteradores) y expresiones generator (que se evalúan en un objeto generator).

De acuerdo con la entrada del glosario para generador parece que la terminología oficial es ahora que generator es la abreviatura de "función de 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 precisos y evitar el término "generador" sin más especificación.

 23
Author: nikow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-06-15 05:04:20

Los generadores podrían considerarse como una abreviatura para crear un iterador. Se comportan como un iterador 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.

Actualización:

Como muchas otras respuestas están mostrando, hay diferentes maneras de crear un generador. Puedes usar la sintaxis de paréntesis como en mi ejemplo anterior, o puedes usar yield. Otra característica interesante es que los generadores pueden ser" infinitos " iter 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
...
 21
Author: overthink,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-18 14:15:52

No hay equivalente en 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 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 el generador se ejecuta, comienza por el principio, de lo contrario continúa desde la vez anterior que se produjo.

 9
Author: Wernsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-20 09:02:05

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

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

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

Con funciones regulares, en algún momento la función devuelve un valor, y la pila es "popped". El marco de 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 el marco de pila sea descartado, usando la sentencia yield. Los valores de las variables locales y el contador del programa dentro de la función se conservan. Esto permite que el generador se reanude en un momento posterior, 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 hacían los generadores. Python 2.5 agregó la capacidad de pasar valores 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 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 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 esto suele ser una ventaja menor.

 7
Author: Peter Hansen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-12-19 10:50:33

Lo único que puedo añadir a la respuesta de Stephan202 es una recomendación de que eches un vistazo a la presentación PyCon '08 de David Beazley "Trucos de generadores para Programadores de Sistemas", que es la mejor explicación del cómo y por qué de los generadores que he visto en cualquier lugar. Esto es lo que me llevó de "Python se ve divertido" a "Esto es lo que he estado buscando."Está en http://www.dabeaz.com/generators/.

 6
Author: Robert Rossney,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-18 17:54:00

Ayuda a hacer una clara distinción 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 resumible.

yield se comporta como return en el sentido de que los valores que se obtienen son "devueltos" por el generador. A diferencia de retorno, sin embargo, la próxima vez que el generador se le pide un valor, la función del generador, foo, reanuda donde lo dejó after después de la última declaración de rendimiento.y continúa ejecutándose hasta que llegue a otra declaración de rendimiento.

Entre bastidores, cuando se llama a bar=foo(6) se define la barra de objetos del generador 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 termina foo (y no hay más valores rendidos), llamar a next(bar) arroja un error de StopInteration.

 5
Author: unutbu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-20 09:04:37

Este post utilizará números de Fibonacci como una herramienta para construir para explicar la utilidad de generadores de Python.

Este post 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++ extremadamente fácilmente:

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, lo hará recalcular 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á.

Así que uno puede ser tentado a reescribir lo anterior manteniendo un registro del estado en 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 utilizar un iterator para iterar sobre ese conjunto de valores, pero esto requiere una gran cantidad de memoria a la vez para un gran número de valores devueltos.

Así que volviendo a nuestro viejo enfoque, ¿qué pasa si queremos hacer algo más aparte de imprimir los números? Tendríamos que copiar y pegar todo el bloque de código en main y cambiar las instrucciones de salida a cualquier otra cosa que quisiéramos hacer. Y si copias y pegas el código, entonces deberías ser disparado. No quieres que te disparen, ¿verdad?

A resolver estos problemas, y para evitar que nos 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, llamaríamos 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 en 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 quisieras obtener los dos primeros números de Fibonacci, y luego ¿hacer algo, luego conseguir más, y luego hacer otra cosa?

Bueno, podríamos seguir como hemos estado, y podríamos empezar a agregar estado de nuevo en main, permitiendo que GetFibNumbers comience desde un punto arbitrario. Pero esto inflará 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 lugar de eso hablemos sobre los 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 de nuevo donde lo dejó. Cada vez devolviendo un valor.

Considere el siguiente código que utiliza 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()

Que nos da los resultados:

0 1 1 2 3 5

La instrucción yield se usa en conjuction con generadores de Python. Guarda el estado de la función y devuelve el valor yeilded. La próxima vez que llame a la función next() en el generador, continuará donde lo dejó el yield.

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

Fuente

 4
Author: Brian R. Bondy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-20 09:19:40

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

Puede que disfrutes de la visión general del Icono, que te permite envolver tu cabeza alrededor de ellos sin concentrarte en la sintaxis (ya que Icon es un lenguaje que probablemente no conoces, y Griswold estaba explicando los beneficios de su lenguaje a personas que vienen de otros idiomas).

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

 2
Author: Nosredna,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-11-18 14:53:32

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

Por ejemplo, el siguiente código de suma construirá una lista completa de cuadrados en 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 mediante el uso de un generator expression instead:

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

Se otorgan beneficios similares a los constructores para 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

 2
Author: Saqib Mujtaba,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-24 18:38:33