2009-11-18 16 views
152

Leyendo el libro de cocina de Python por el momento y mirando actualmente los generadores. Me resulta difícil entenderlo.Descripción de los generadores en Python

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.

¿Alguien puede explicar qué es un generador y por qué lo usaría? 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!

+2

Marque esta answer http://stackoverflow.com/a/23530101/736037 – Giri

Respuesta

287

Nota: este post asume la sintaxis de Python 3.x.& dagger;

Un generator es simplemente una función que devuelve un objeto sobre el que se puede llamar next, de manera que para cada llamada que devuelve algún valor, hasta que se lanza una excepción StopIteration, lo que indica 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, existe una alternativa, llamada yield. Usar yield en cualquier lugar de una función lo convierte en un generador. Observar 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 se puede ver, myGen(n) es una función que produce n y n + 1. Cada llamada a next produce un único valor, hasta que todos los valores hayan sido cedidos. for bucles llaman next en el fondo, por lo tanto:

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

Del mismo modo que hay generator expressions, 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 

en cuenta que las expresiones generadoras se parecen mucho a list comprehensions :

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

Observe que se genera un objeto generador una vez, pero su código es no ejecutar todo a la vez. Solo las llamadas al next realmente ejecutan (parte del) código.La ejecución del código en un generador se detiene una vez que se ha alcanzado una declaración yield, sobre la cual devuelve un valor. La siguiente llamada al next hace que la ejecución continúe en el estado en que quedó el generador después del último 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. Es, por ejemplo, posible a send datos devueltos a un generador (reference). Pero eso es algo que sugiero que no estudies hasta que entiendas el concepto básico de un generador.

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

  • Ciertos conceptos se pueden describir mucho más sucintamente utilizando generadores.
  • En lugar de crear una función que devuelve una lista de valores, se puede escribir un generador que genera los valores sobre la marcha. Esto significa que no es necesario compilar ninguna lista, lo que significa que el código resultante es más eficiente en cuanto a la memoria. De esta forma, uno puede incluso describir flujos de datos que simplemente serían demasiado grandes para caber en la memoria.
  • Los generadores permiten una forma natural de describir infinitos flujos. Consideremos por ejemplo el Fibonacci numbers:

    >>> 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 un flujo infinito. Se le aconseja que consulte las funciones del módulo itertools, ya que son herramientas esenciales para escribir generadores avanzados con gran facilidad.


    y daga;Acerca de Python < = 2.6: en los ejemplos anteriores next es una función que llama al método __next__ en el objeto dado. En Python < = 2.6 se utiliza una técnica ligeramente diferente, es decir, o.next() en lugar de next(o). Python 2.7 tiene next() llamada .next por lo que no es necesario utilizar el siguiente en 2.7:

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

Usted menciona que es posible 'enviar' datos a un generador. Una vez que haces eso, tienes una 'corutina'. Es muy simple implementar patrones como el consumidor/productor mencionado con coroutines porque no necesitan 'Lock's y por lo tanto no pueden interbloqueo. Es difícil describir corutinas sin atacar los hilos, así que solo diré que las corotines son una alternativa muy elegante al enhebrado. –

+0

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

+1

Esta es una respuesta increíble que vale la pena marcar. –

20

Los generadores podrían considerarse como una abreviatura para 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.

Actualización:

Como muchas otras respuestas están mostrando, hay diferentes maneras de crear un generador. Puede usar la sintaxis de paréntesis como en mi ejemplo anterior, o puede usar yield. Otra característica interesante es que los generadores pueden ser "infinita" - 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 
... 
+0

Ahora, Java tiene 'Stream's, que son mucho más similares a los generadores, excepto que aparentemente no se puede obtener el siguiente elemento sin una sorprendente cantidad de molestias . –

8

No hay equivalente en Java.

Aquí es 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 va desde 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 al principio; de lo contrario, continúa desde la hora anterior

+2

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

+0

No existe un equivalente sintáctico en Java para una "expresión de generador", pero los generadores, una vez que tienes uno, son esencialmente solo un iterador (las mismas características básicas que un iterador de Java). – overthink

+0

@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 + 1' en mi ejemplo," hello "se imprimiría 100 veces, mientras que el cuerpo del ciclo for se seguiría ejecutando 33 veces. – Wernsey

5

Ayuda a distinguir claramente 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 utilizar un objeto generador está en un bucle:

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

Las impresiones de bucle

# 6 
# 7 

pensar en un generador como una función resumable.

yield se comporta como return en 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 solicita un valor al generador, la función del generador, foo, se reanuda donde 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 al bar=foo(6), la barra de objetos del generador se define para que tenga un atributo next.

Se le puede llamar a sí mismo para recuperar valores obtenidos a partir foo:

next(bar) # works in python2.6 or python3.x 
bar.next() # works in python2.5+, but is deprecated. Use next() if possible. 

Cuando extremos foo (y hay valores no más rindió), llamando next(bar) lanza un error de StopInteration.

39

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í sucesivamente. El (o uno) beneficio de los generadores es que debido a que manejan los 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. Generadores, al igual que las listas, son iterable, para que puedan ser utilizados 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 manera 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 una ciclo infinito, pero esto no es un problema porque solo recibes cada respuesta cada vez que lo pides.

19

En primer lugar, el término generador originalmente estaba algo mal definido en Python, lo que genera mucha confusión. Lo que probablemente quiere decir es iteradores y iterables (vea here). Luego en Python también hay funciones de generador (que devuelven un objeto generador), objetos generadores (que son iteradores) y expresiones generadoras (que se evalúan en un objeto generador).

De acuerdo con http://docs.python.org/glossary.html#term-generator parece que la terminología oficial es ahora que generador es la abreviatura de "generador de función". En el pasado, la documentación definía los términos de forma inconsistente, pero, afortunadamente, esto se ha solucionado.

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

+1

Hmm Creo que tienes razón, al menos según una prueba de unas pocas líneas en Python 2.6. Una expresión de generador devuelve un iterador (también conocido como "objeto generador"), no un generador. –

2

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

Puede disfrutar de the Icon overview, que le permite rodear su cabeza sin concentrarse en la sintaxis (ya que Icon es un lenguaje que probablemente desconoce, y Griswold explicaba los beneficios de su lenguaje a las personas procedentes de otros idiomas)

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

6

Lo único que puedo agregar a la respuesta de Stephan202 es una recomendación de echar 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 lado 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/.

2

En esta publicación se usará Fibonacci numbers como una herramienta para compilar para explicar la utilidad de Python generators.

Esta publicación incluirá códigos C++ y Python.

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 puede ser transferido a una función de C++ muy 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 6 primeros números de Fibonacci, se le recalcular una gran cantidad 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 tener la tentación de volver a escribir lo anterior al realizar un seguimiento del estado en main.

//Not supported for the first 2 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 que 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 devueltos.

Volviendo a nuestro enfoque anterior, ¿qué sucede si queremos hacer algo más 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, entonces debería recibir un disparo. No quieres que te disparen, ¿verdad?

Para resolver estos problemas, y para evitar recibir una vacuna, podemos volver a escribir este bloque de código utilizando 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, en su lógica de main no es tan desordenado, y se puede hacer lo que quiera con los números de Fibonacci, sólo tiene que definir nuevas devoluciones de llamada.

Pero esto todavía no es perfecto. ¿Qué pasa si solo quiere obtener los primeros 2 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 estado de nuevo en main, lo que permite a GetFibNumbers comenzar desde un punto arbitrario. Pero esto hinchará 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 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 de nuevo donde lo dejó. Cada vez que devuelve 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() 

Lo que nos da los resultados:

La declaración yield se usa junto con los generadores Python. Guarda el estado de la función y devuelve el valor levantado. La próxima vez que llame a la función next() en el generador, continuará donde dejó el rendimiento.

Esto es mucho más limpio que el código de la 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 números enteros arbitrariamente grandes).

Source

3

Me gusta describir los generadores, a los que tienen un fondo decente en lenguajes de programación y la informática, en términos de marcos de pila.

En muchos idiomas, hay una pila encima de la cual está el "marco" de la pila actual. El marco de pila incluye 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 se transfiere al comienzo de la función a la que se llama.

Con funciones regulares, en algún punto la función devuelve un valor y la pila se "extrae".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 descartando el marco de pila, usando la declaración de rendimiento. 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 se encuentra disponible como una expresión resultante de la declaración de rendimiento que temporalmente ha devuelto 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 pila, se pierde todo ese "estado". Una ventaja secundaria es que se evita parte de la sobrecarga de llamada de función (creación y eliminación de fotogramas de pila), aunque esta suele ser una ventaja menor.

0

La experiencia con la lista de comprensiones ha demostrado su amplia utilidad 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 sumatoria va a construir una lista completa de las plazas en la memoria, iterar sobre esos valores, y, cuando ya no es necesaria la referencia, borrar la lista:

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

Memoria se conserva mediante el uso de una expresión generador en lugar:

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

beneficios similares se confieren 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) 

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

more

Cuestiones relacionadas