iteradores - que es un iterable en python
Comprender los generadores en Python (11)
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 está en un bucle:
for n in foo(6):
print(n)
El lazo 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 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 llamas a bar=foo(6)
la barra de objetos del generador está definida para que tengas un next
atributo.
Puede llamarlo usted mismo para recuperar los valores obtenidos de foo:
next(bar) # works in python2.6 or python3.x
bar.next() # works in python2.5+, but is deprecated. Use next() if possible.
Cuando foo finaliza (y no hay más valores arrojados), la llamada next(bar)
arroja un error StopInteration.
Leyendo el libro de cocina de Python en el momento y actualmente 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.
¿Alguien puede explicar 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!
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 la descripción general del icono , que le permite abarcar su cabeza sin concentrarse en la sintaxis (ya que Icon es un idioma que probablemente desconozca, y Griswold explicaba los beneficios de su lenguaje a personas provenientes de otros idiomas).
Después de leer unos pocos párrafos allí, la utilidad de los generadores e iteradores podría ser más evidente.
En primer lugar, el generador de términos originalmente estaba mal definido en Python, lo que generó mucha confusión. Lo que probablemente quieras decir son iteradores e iterables (mira here ). Luego, en Python también hay funciones de generador (que devuelven un objeto de generador), objetos de generador (que son iteradores) y expresiones de generador (que se evalúan para un objeto de generador).
De acuerdo con http://docs.python.org/glossary.html#term-generator , parece que la terminología oficial es que ahora el generador es la abreviatura de "función del generador". 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.
Esta publicación usará los números de Fibonacci como una herramienta para construir y explicar la utilidad de los generator .
Esta publicación incluirá códigos 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 ++ con suma facilidad:
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 6 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á.
Entonces, uno puede tener la tentación de volver a escribir lo anterior siguiendo el 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 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 copias y pegas el código, entonces debes recibir un disparo. No quieres que te disparen, ¿verdad?
Para resolver estos problemas, y para evitar recibir un disparo, 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, tu lógica main
no es tan abarrotada, y puedes hacer lo que quieras con los números de Fibonacci, simplemente define nuevas devoluciones de llamadas.
Pero esto aún 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 continuar como lo hemos hecho, y podríamos comenzar a agregar estado nuevamente en main
, permitiendo 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 cambio, hablemos de generadores.
Python tiene una función de lenguaje muy agradable que resuelve problemas como estos llamados generadores.
Un generador te permite ejecutar una función, detenerte en un punto arbitrario y luego continuar de nuevo donde lo dejaste. Cada vez que regresas 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 cual nos da los resultados:
0
1
1
2
3
5
La declaración de 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).
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 de suma acumulará una lista completa de cuadrados en la memoria, iterará sobre esos valores y, cuando la referencia ya no se necesite, elimine la lista:
sum([x*x for x in range(10)])
La memoria se conserva mediante el uso de una expresión de generador en su lugar:
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 de generador son especialmente útiles con funciones como sum (), min () y max () que reducen una entrada iterable a un único valor:
max(len(line) for line in file if line.strip())
Lo único que puedo agregar a la respuesta de Stephan202 es una recomendación de que eche un vistazo a la presentación PyCon ''08 de David Beazley "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/ .
Los generadores podrían considerarse como 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 muchas otras respuestas se muestran, hay diferentes formas 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 "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
...
Me gusta describir generadores, para aquellos con un buen historial en lenguajes de programación e 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 "dispara". 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 se descarte, utilizando 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 de vuelta 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.
No hay equivalente en Java.
Aquí hay 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 de 0 a n, y si la variable de bucle es un múltiplo de 3, produce la variable.
Durante cada iteración del ciclo for se ejecuta el generador. Si es la primera vez que se ejecuta el generador, comienza al principio; de lo contrario, continúa desde el momento anterior en que se produjo.
Un generador es efectivamente una función que devuelve (datos) antes de que finalice, pero hace una pausa en ese punto, y usted 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 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. 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 ciclo infinito, pero esto no es un problema porque solo obtiene cada respuesta cada vez que lo solicita.
Nota: esta publicación asume la sintaxis de Python 3.x. †
Un generator es simplemente una función que devuelve un objeto al que puede llamar a next
, de modo que para cada llamada devuelve algún valor, hasta que genera una excepción StopIteration
, que indica que se han generado todos los valores. Tal objeto se llama iterador .
Las funciones normales devuelven un único valor usando return
, al igual que en Java. En Python, sin embargo, existe una alternativa, llamada yield
. Usar el yield
en cualquier lugar 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 "<stdin>", line 1, in <module>
StopIteration
Como puede ver, myGen(n)
es una función que produce n
y n + 1
. Cada llamada al next
produce un solo valor, hasta que todos los valores hayan sido cedidos. for
bucles llame al next
en segundo plano, por lo tanto:
>>> for n in myGen(6):
... print(n)
...
6
7
Del mismo modo, hay expresiones de generador , que proporcionan un medio para describir brevemente 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 a las listas de comprensión :
>>> 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 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 de yield
, sobre la cual devuelve un valor. La siguiente llamada al next
hace que la ejecución continúe en el estado en el 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. Por ejemplo, es posible send
datos de vuelta 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, uno 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 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 tener una buenaitertools
las funciones en el móduloitertools
, ya que son herramientas esenciales para escribir generadores avanzados con gran facilidad.
† Acerca de Python <= 2.6: en los ejemplos anteriores, next
encuentra una función que llama al método __next__
en el objeto dado. En Python <= 2.6 uno usa una técnica ligeramente diferente, es decir, o.next()
lugar de next(o)
. Python 2.7 tiene next()
llamada .next
así que no necesita usar lo siguiente en 2.7:
>>> g = (n for n in range(3, 5))
>>> g.next()
3