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 StopIteration
excepció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 yield
en 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 n
y n + 1
. Cada llamada a next
produce un valor único, hasta que se hayan producido todos los valores. for
los bucles llaman next
al 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 next
ejecutar realmente (parte de) el código. La ejecución del código en un generador se detiene una vez yield
que se ha alcanzado una declaración, sobre la cual devuelve un valor. La siguiente llamada a next
hace 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 send
volver 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.islice
para tomar un número finito de elementos de una secuencia infinita. Se recomienda observar bien las funciones del itertools
módulo, ya que son herramientas esenciales para escribir generadores avanzados con gran facilidad.
† Acerca de Python <= 2.6: en los ejemplos anteriores 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, en o.next()
lugar de next(o)
. Python 2.7 tiene next()
llamada, .next
por lo que no necesita usar lo siguiente en 2.7:
>>> g = (n for n in range(3, 5))
>>> g.next()
3
send
datos 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 deLock
s 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.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.
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:
Tenga en cuenta que los generadores proporcionan otra forma de lidiar con el infinito, por ejemplo
El generador encapsula un bucle infinito, pero esto no es un problema porque solo obtienes cada respuesta cada vez que la solicitas.
fuente
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.
fuente
Se podría considerar que los generadores son una forma abreviada de crear un iterador. Se comportan como un iterador de Java. Ejemplo:
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:
fuente
Stream
s, que son mucho más similares a los generadores, excepto que aparentemente no puede obtener el siguiente elemento sin una cantidad sorprendente de problemas.No hay equivalente de Java.
Aquí hay un pequeño ejemplo artificial:
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
for
bucle 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.fuente
print "hello"
después dex=x+1
en mi ejemplo, "hola" se imprimiría 100 veces, mientras que el cuerpo del bucle for solo se ejecutaría 33 veces.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.
fuente
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/ .
fuente
Ayuda a hacer una distinción clara entre la función foo y el generador foo (n):
Foo es una función. foo (6) es un objeto generador.
La forma típica de usar un objeto generador es en un bucle:
El bucle imprime
Piense en un generador como una función reanudable.
yield
se comporta comoreturn
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 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 unnext
atributo.Puede llamarlo usted mismo para recuperar los valores obtenidos de foo:
Cuando termina foo (y no hay más valores cedidos), la llamada
next(bar)
arroja un error StopInteration.fuente
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:
Esto se puede transferir a una función C ++ extremadamente fácil:
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)
peroFib(2)
también recalculaFib(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
.Pero esto es muy feo y complica nuestra lógica
main
. Sería mejor no tener que preocuparse por el estado en nuestramain
función.Podríamos devolver un
vector
valor y usar uniterator
para 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
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 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.
Esto es claramente una mejora, su lógica
main
no 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:
Lo que nos da los resultados:
La
yield
declaració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
fuente
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.
fuente
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:
Las expresiones generadoras son especialmente útiles con funciones como sum (), min () y max () que reducen una entrada iterable a un solo valor:
más
fuente
Puse este código que explica 3 conceptos clave sobre generadores:
fuente