Link Search Menu Expand Document

Generators en Python

Si alguna vez te has encontrado con una funci贸n en Python que no s贸lo tiene una sentencia return, sino que adem谩s devuelve un valor haciendo uso de yield, ya has visto lo que es un generador o generator. A continuaci贸n te explicamos c贸mo se crean, para qu茅 sirven y sus ventajas. Es muy importante tambi茅n no confundir los generadores con las corrutinas, que tambi茅n usan yield pero de otra manera, sin embargo estas las dejamos para otro post.

Empecemos por lo b谩sico. Seguramente ya sepas lo que es una funci贸n y c贸mo puede devolver un valor con return.

def funcion():
    return 5

Como hemos explicado, los generadores usan yield en vez de return. Vamos a ver que pasar铆a si cambiamos el return por el yield.

def generador():
    yield 5

Y ahora vamos a intentar llamar a las dos 鈥渇unciones鈥.

print(funcion())
print(generador())
# Salida: 5
# Salida: <generator object generador at 0x103e7f0a0>

Podemos ver ya la primera diferencia al usar el yield. En el primer caso, se devuelve un 5, pero en el segundo lo que se devuelve es en realidad un objeto de la clase generator. En realidad el n煤mero 5 tambi茅n puede ser accedido en el caso del generador, pero esto lo veremos m谩s adelante.

Entonces, si una funci贸n contiene al menos una sentencia yield, se convertir谩 en una funci贸n generadora. Una funci贸n generadora se diferencia de una funci贸n normal en que tras ejecutar el yield, la funci贸n devuelve el control a qui茅n la llam贸, pero la funci贸n es pausada y el estado (valor de las variables) es guardado. Esto permite que su ejecuci贸n pueda ser reanudada m谩s adelante.

Iterando los Generadores

A continuaci贸n vamos a ver c贸mo acceder a los valores del generador. Para entenderlo mejor, te recomendamos que leas antes m谩s acerca de iterables e iteradores.

Otra de las caracter铆sticas que hacen a los generators diferentes, es que pueden ser iterados, ya que codifican los m茅todos __iter__() y __next__(), por lo que podemos usar next() sobre ellos. Dado que son iterables, lanzan tambi茅n un StopIteration cuando se ha llegado al final.

Volviendo al ejemplo anterior, vamos a ver como podemos usar el next().

a = generador()
print(next(a))
# Salida: 5

Como te prometimos antes, el 5 tambi茅n se pod铆a acceder 驴has visto?. Pero vamos a ver que pasa ahora si intentamos llamar otra vez a next(). Si recuerdas, s贸lo tenemos una llamada a yield.

a = generador()
print(next(a))
print(next(a))
# Salida: 5
# Salida: Error! StopIteration:

Como era de esperar, tenemos una excepci贸n del tipo StopIteration, ya que el generador no devuelve m谩s valores. Esto se debe a que cada vez que usamos next() sobre el generador, se llama y se contin煤a su ejecuci贸n despu茅s del 煤ltimo yield. Y en este caso c贸mo no hay m谩s c贸digo, no se generan m谩s valores.

Creando Generadores

Vamos a ver otro ejemplo m谩s completo donde tengamos un generador que genere varios valores. En la siguiente funci贸n podemos ver como tenemos una variable n que incrementada en 1, y despu茅s retorna con yield. Lo que pasar谩 aqu铆, es que el generador generar谩 un total de tres valores.

def generador():
    n = 1
    yield n

    n += 1
    yield n

    n += 1
    yield n

Y haciendo uso de next() al igual que hac铆amos antes, podemos ver los valores que han sido generados. Lo que pasa por debajo, ser铆a lo siguiente:

  • Se entra en la funci贸n generadora, n=1 y se devuelve ese valor. Como ya hemos visto, el estado de la funci贸n se guarda (el valor de n es guardado para la siguiente llamada)
  • La segunda vez que usamos next() se entra otra vez en la funci贸n, pero se contin煤a su ejecuci贸n donde se dej贸 anteriormente. Se suma 1 a la n y se devuelve el nuevo valor.
  • La tercera llamada, realiza lo mismo.
  • Una cuarta llamada dar铆a un error, ya que no hay m谩s c贸digo que ejecutar.
g = generador()
print(next(g))
print(next(g))
print(next(g))
# Salida: 1, 2, 3

Otra forma m谩s c贸moda de realizar lo mismo, ser铆a usando un simple bucle for, ya que el generador es iterable.

for i in generador():
    print(i)
# Salida: 1, 2, 3

Forma alternativa

Los generadores tambi茅n pueden ser creados de una forma mucho m谩s sencilla y en una sola l铆nea de c贸digo. Su sintaxis es similar a las list comprehension, pero cambiando el corchete [] por par茅ntesis ().

El ejemplo con list comprehensions ser铆a el siguiente. Simplemente se generan los valores de una lista elevados al cuadrado.

lista = [2, 4, 6, 8, 10]
al_cuadrado = [x**2 for x in lista]
print(al_cuadrado)
# [4, 16, 36, 64, 100]

Y su equivalente con generadores ser铆a lo siguiente.

al_cuadrado_generador = (x**2 for x in lista)
print(al_cuadrado_generador)
# Salida: <generator object <genexpr> at 0x103e803b8>

Y como hemos visto los valores pueden ser generados de la siguiente forma.

for i in al_cuadrado_generador:
    print(i)
# Salda: 4, 16, 36, 64, 100

La diferencia entre el ejemplo usando list compregensions y generators es que en el caso de los generadores, los valores no est谩n almacenados en memoria, sino que se van generando al vuelo. Esta es una de las principales ventajas de los generadores, ya que los elementos s贸lo son generados cuando se piden, lo que hace que sean mucho m谩s eficientes en lo relativo a la memoria.

Ventajas y ejemplos

Llegados a este punto tal vez te preguntes para qu茅 sirven los generadores. Lo cierto es que aunque no existieran, podr铆a realizarse lo mismo creando una clase que implemente los m茅todos __iter__() y __next__(). Veamos un ejemplo de una clase que genera los primeros 10 n煤meros (0,9) al cuadrado.

class AlCuadrado:
    def __init__(self):
        self.i = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.i > 9:
            raise StopIteration

        result = self.i ** 2
        self.i += 1
        return result

Y de la misma forma que us谩bamos los generadores, podemos usar nuestra clase AlCuadrado. Creamos un objeto de ella, y la iteramos. En cada iteraci贸n generar谩 un nuevo valor nuevo hasta que se llegue al final.

eleva_al_cuadrado = AlCuadrado()
for i in eleva_al_cuadrado:
    print(i)
#0,1,4,9,16,25,36,49,64,81

Sin embargo esta forma es un tanto larga y tal vez confusa. Como hemos visto antes, podemos llegar a hacer lo mismo en una sola l铆nea de c贸digo. 驴Para que complicarse la vida?

Por otro lado, ya hemos mencionado que el uso de los generadores hace que no todos los valores est茅n almacenados en memoria sino que sean generados al vuelo. Vamos a ver un ejemplo donde se puede ver mejor. Supongamos que queremos sumar los primeros 100 n煤meros naturales (referencia). Una opci贸n podr铆a ser crear una lista de todos ellos y despu茅s sumarla. En este caso, todos los valores son almacenados en memoria, algo que podr铆a ser un problema si por ejemplo intentamos sumar los primeros 1e10 n煤meros.

def primerosn(n):
    nums = []
    for i in range(n):
        nums.append(i)
    return nums
    
print(sum(firstn(100)))
# Salida: 4950

Sin embargo, podemos realizar lo mismo con un generador. En este caso los valores ser谩n generados uno por uno seg煤n se vayan necesitando.

def primerosn(n):
    num = 0
    for i in range(n):
        yield num
        num += 1
print(sum(primerosn(100)))
# Salida 4950

N贸tese que es un ejemplo con fines did谩cticos, por lo que si quieres hacer esto, la mejor manera ser铆a usando un simple range() asumiendo que usas Python 3.

print(sum(range(100)))
# Salida: 4950