Un generator es una función o una expresión que devuelve un tipo especial de iterador llamado iterador de generador.
Generator-iterators son perezosos: no almacenan sus values en memoria, sino que generan sus valores cuando se necesitan.
Una función generadora tiene el mismo aspecto que cualquier otra función, pero contiene una o más expresiones yield.
Cada yield suspende la ejecución del código y guarda el estado de ejecución actual (incluidas todas las variables locales y las instrucciones try).
Cuando el generador se reanuda, retoma el estado desde el punto en que se suspendió, a diferencia de las funciones normales, que se reinician con cada llamada.
Los generadores se construyen de forma muy parecida a otras funciones con bucles o recursivas, pero requieren una expresión yield, que veremos en detalle un poco más adelante.
Un ejemplo es una función que devuelve los cuadrados de un array de números dado.
Tal como está escrita ahora, hay que procesar toda la entrada antes de poder devolver algún valor:
>>> def squares(list_of_numbers):
... squares = []
... for number in list_of_numbers:
... squares.append(number ** 2)
... return squares
Puedes convertir esa función en un generador así:
>>> def squares_generator(list_of_numbers):
... for number in list_of_numbers:
... yield number ** 2
La razón de ser de todo esto es que usas un generador cuando no necesitas todos los valores a la vez.
Esto ahorra memoria y potencia de procesamiento, ya que solo se calcula el valor con el que estás trabajando en ese momento.
Los generadores (técnicamente, generator-iterators; consulta la nota que aparece a continuación.) son un tipo de iterator y se pueden usar en cualquier parte de Python donde se espere un iterator o un iterable.
Esto incluye funciones u objetos que requieren un iterable/iterator como argumento.
Para profundizar más, consulta Cómo crear un iterador en Python.
Los iteradores de generador son un subconjunto especial de los iteradores.
Los Iterators son el mecanismo o protocolo que permite recorrer los iterables con bucles.
Los iteradores de generador y los iteradores que devuelven los iterables habituales de Python se comportan de forma muy parecida, pero hay algunas diferencias importantes que conviene tener en cuenta:
indexes.
No puedes referenciar un valor anterior o futuro mediante suma o resta, y no puedes usar la notación de corchetes ([]) ni el slicing.len(), ya que no tienen longitud.generator-iterator infinito!Para usar el generador squares_generator():
>>> squared_numbers = squares_generator([1, 2, 3, 4])
>>> for square in squared_numbers:
... print(square)
...
1
4
9
16
Los valores de un generador también se pueden producir o consultar mediante la función next().
next() llama al método __next__() de un objeto generador y «avanza» o evalúa el código del generador hasta su expresión yield, que entonces «produce» o devuelve el valor.
>>> squared_numbers = squares_generator([1, 2])
>>> next(squared_numbers)
1
>>> next(squared_numbers)
4
Cuando un generator se ha consumido por completo y ya no tiene más valores que devolver, lanza un error StopIteration.
>>> next(squared_numbers)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Los generadores son un subconjunto especial de los iteradores.
Los Iterators son el mecanismo o protocolo que permite recorrer los iterables con bucles.
Los generadores y los iteradores que devuelven los iterables habituales de Python se comportan de forma muy parecida, pero hay algunas diferencias importantes que conviene tener en cuenta:
Los generadores son unidireccionales; no se puede «retroceder» a un valor anterior.
Iterar sobre los generadores consume los valores devueltos; no hay reinicio.
Los generadores (que tienen evaluación perezosa) no se pueden ordenar ni invertir.
Los generadores no tienen indexes, así que no puedes referenciar un valor anterior o futuro mediante suma o resta.
Los generadores no se pueden usar con la función len().
Los generadores pueden ser finitos o infinitos; ten cuidado al recopilar todos los valores de un generador infinito.
La expresión yield es muy similar a la expresión return.
A diferencia de la expresión return, yield cede valores a quien lo llama en un punto concreto, suspendiendo la evaluación o la devolución de cualquier valor adicional hasta que se soliciten.
Cuando se evalúa yield, pausa la ejecución de la función que lo contiene y devuelve los valores de la función en ese momento concreto.
La función se mantiene en el scope y, cuando se llama a __next__(), la ejecución se reanuda hasta que se vuelve a encontrar yield.
El uso de expresiones yield está prohibido fuera de las funciones.
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
>>> lets_try = infinite_sequence()
>>> lets_try.__next__()
0
>>> lets_try.__next__()
1
Los generadores son útiles en muchas aplicaciones.
Cuando trabajas con una colección grande, puede que no quieras poner todos sus valores en la memory.
Un generador se puede usar para trabajar con datos más grandes pieza a pieza, ahorrando memoria y mejorando el rendimiento.
Los generadores también resultan muy útiles cuando un proceso o un cálculo es complejo, costoso o infinito:
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
Ahora, cada vez que se llama a __next__() en el objeto infinite_sequence, devolverá el número anterior + 1.