Aún no tienes acceso a esta clase

Crea una cuenta y continúa viendo este curso

Curso Profesional de Python

Curso Profesional de Python

Facundo García Martoni

Facundo García Martoni

Generadores

14/21
Recursos

Aportes 40

Preguntas 10

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesión.

Yo uso generadores para retornar un resultado de una consulta en BigQuery. Como son muchísimos datos, es ideal iterar sobre el resultado de manera lazy

Generadores

Sugar syntax de los iteradores. Los generadores son funciones que guardan un estado. Es un iterador escrito de forma más simple.

def my_gen():

  """un ejemplo de generadores"""

  print('Hello world!')
  n = 0
  yield n # es exactamente lo mismo que return pero detiene la función, cuando se vuelva a llamar a la función, seguirá desde donde se quedó

  print('Hello heaven!')
  n = 1
  yield n

  print('Hello hell!')
  n = 2
  yield n


a = my_gen()
print(next(a)) # Hello world!
print(next(a)) # Hello heaven!
print(next(a)) # Hello hell!
print(next(a)) StopIteration

Ahora veremos un generator expression (es como list comprehension pero mucho mejor, porque podemos manejar mucha cantidad
de información sin tener problemas de rendimiento):

#Generator expression

my_list = [0,1,4,7,9,10]

my_second_list = [x*2 for x in my_list] #List comprehension
my_second_gen = ()x*2 for x in my_list]) #Generator expression

En el minuto 5:44 más bien seria el doble, no el cuadrado.

Python internamente utiliza operaciones con generadores para aumentar el rendimiento.
Tal es el caso de máx , min o sum.
Ejemplo:

suma = sum([i for i in range(100)]) # Crea la lista en memoria.
suma_gen = sum(i for i in range(100)) #Itera sobre los valores.

El el primer caso, creamos una lista que no nos servirá y se termina guardando el espacio en memoria, en el segundo caso se itera cada valor hasta obtener el resultado sin la necesidad de guardar en memoria cada valor.

Generator Expression

Es lo mismo que un iterador pero escrito de forma más elegante.

Un “list comprehension” es una manera sencilla de implementar un ciclo el cual nos permite crear una nueva lista con elementos basados en los valores de una lista existente. Dicha lista creada almacena cada uno de estos nuevos valores.

A diferencia de un List comprehension que puede ocupar mucha memoria pues cada valor es almacenado en la variable, un Generator expression solo los recorre, sin guardarlos en una lista nueva. Permite traer un elemento a la vez cuando se recorra usando un ciclo for.

# Generator expression

my_list = [0, 1, 4, 7, 9, 10]

my_second_list = [x*2 for x in my_list] # List comprehension
my_second_gen = (x*2 for x in my_list)  # Generator expression

Como se puede ver en el ejemplo, la diferencia en la sintaxis únicamente es en el uso de paréntesis en lugar del uso de corchetes.

Ventajas de los Generadores:

  • Es mas fácil de escribir que un iterador
  • Ahorra Tiempo y Memoria
  • Permite guardar secuencias infinitas

Los Generadores son iteradores con Sugar Syntax.

Yield es una palabra clave que se usa para retornar de una función sin destruir los estados de las variables locales y cuando se llama a la función, la ejecución comienza desde el último yield declarado. Toda función que contenga la palabra clave yield es denominada como un generador.

Buenos días.

Mi script para ver la diferencia del tiempo
al ejecutarse una list comprehension y un iterator expression

from datetime import datetime

LIST = [1, 2, 4, 5, 20, 100, 500, 1000]

def list_comprehension():
    """Function run as start"""
    time_start = datetime.now()
    list_comprehension = [x*2 for x in LIST]
    time_final = datetime.now()
    time = time_final -time_start
    print(f'Time for create list comprehension: {time}')
    
    print(f'list_comprehension: {list_comprehension}')
    return time


def generator_expression():
    """Function run as start"""
    time_start = datetime.now()
    generator_expression = (x*2 for x in LIST)
    time_final = datetime.now()
    time = time_final -time_start
    
    for i in generator_expression:
        print(i)

    print(f'Time for create generator espression: {time}')
    return time


def run():
    """Function run as start"""
    x = list_comprehension()
    y = generator_expression()
    print(f'Difference: {x - y}')


if __name__ == '__main__':
    run()

  • yield, es similar a return, con la diferencia que la próxima vez que se llame a la función, esta continuará desde el último yield.
  • Un generador es similar a un iterador.

lista de numeros impares VS generador de numeros impares

import time

impares = [2*i+1 for i in range(100)] # Crea la lista en memoria de numeros impares
impares_gen = (2*i+1 for i in range(100)) #Itera sobre los numeros impares 


print(impares)
print(type(impares))


print(impares_gen)
print(type(impares_gen))

for i in impares_gen:
    print(i)
    time.sleep(1)

si ejecutamos el siguiente código:

if __name__ == "__main__":
    my_list = [1, 2, 3, 4]
    new_list = [x for x in my_list]
    new_gene = (x for x in my_list)

    print(next(new_gene))
    print(next(new_gene))
    print(next(new_gene))
    print(next(new_gene))

obtenemos una secuencia de números

# 1
# 2
# 3
# 4

Al ejecutarlo así me fue mas fácil comprenderlo

Generadores

  • Los iteradores tienen sugar syntax 💖. Son llamados generadores, los cuales son funciones que guardan un estado 🤔.

  • Los generadores son funciones: 🔥

    def my_gen():
    	"""Un ejemplo de generadores"""
    	print("Hello world!")
    	n = 0
    	yield n 
    	
    	print("Hello heaven!")
    	n = 1
    	yield n
    
    	print("Hello hell!")
    	n = 2
    	yield n
    
    # Lo instanciamos
    a = my_gen()
    print(next(a)) # Hello world!
    print(next(a)) # Hello heaven!
    print(next(a)) # Hello hell!
    print(next(a)) # StopIteration
    
  • yield este keyword es análoga al return, pero no termina la función, le pone pausa. La siguiente ejecución, corre después de este punto 👀.

  • Generator expression. 🤯 Se trae un elemento a la vez, y no ocupa toda la memoria 😯.

    my_list = [0, 1, 2, 3, 4, 5, 6]
    
    my_second_list = [x*2 for x in my_list] # List comprehension
    my_second_gen = (x*2 for x in my_list) # Generator expression
    
  • La ventaja de los generadores es que es más fácil de escribir que un iterador, y tiene las mismas ventajas que este último. 💕

Que alegria poder conocer este tema, simplifica mucho la sintaxis que la de un iterador

Ventajas de los Generadores:

  • Es mas fácil de escribir que un iterador
  • Ahorra Tiempo y Memoria
  • Permite guardar secuencias infinitas

¡Qué buen tema! Claramente el saber no ocupa lugar y una vez estudiado tenemos tres obligaciones: practicar, practicar y practicar… Siempre practicar.

Que cosa tan rara, nunca había visto ese yield, no se me ocurre por lo pronto una utilidad, pero seguro la debe tener, debe ser difícil acceder a una función que no sabes que va a retornar

La principal diferencia entre List Comprehesion y Generator Expression además de los [ ] y ( ). Es que la List Comprehesion realiza todas las operaciones y luego de terminar muestra el resultado, mientras que el Generator Expression hace la operación solo si se le pide, y va una a una. ¿Es así?

Los generadores ahorran memoria y tiempo de ejecución al obtener los elementos de la estructura (iterador) solo cuando se le solicite.

#APUNTES DE LA CLASE 🐍

Los GENERADORES son un “Sugar syntax” de los iteradores. Los generadores son funciones que guardan un estado.

  • Se usa yield el cual es como un return pero pausa la función hasta donde estaba el yield. Empieza desde donde se llamó el último yield, La función guarda un estado
def my_gen():
    """ Un ejemplo de generadores"""
    print('Hello world')
    n = 0
    yield n
    
    print("hello heaven")
    n = 1
    yield n
    
    print("hello hell")
    n = 2
    yield n
    
a = my_gen()

print(next(a)) 
print(next(a))
print(next(a)) 
print(next(a)) StopIteration

Cada vez que ejecuto next(a) el código ejecuta desde el yield donde está. Cuando se acaban los yield usamos el StopIteration

Generator expression

Trae un elemento a la vez cuándo se recorre

my_list = [0,1,2,3,4,5]

my_second_list = [x*2 for x in my_list]
my_second_gen = (x*w for x in my_list)

Fibonacci con Generators

def fibonacci(max = None):
    n1 = 0
    if max is not None and n1 > max:
        return

    yield n1

    n2 = 1

    if max is not None and n2 > max:
        return
    
    yield n2

    while True:
        output = n1 + n2

        if max is not None and output > max:
            return
        
        n1 = n2
        n2 = output

        yield output


for i in fibonacci(100):
    print(i)

Creo que nunca terminaré de aprender todas las palabras reservadas de python

Mas explicaciones sobre generadores pueden buscar aquí

https://ellibrodepython.com/yield-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 “funciones”.

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

Sugar Syntax fue lo mejor que descubrí este 2021.

Los generadores son azúcar sintáctica para los Iteradores. Son básicamente funciones que guardan un estado, a diferencia de los iteradores, que son clases.

Ejemplo:

def my_gen():
	"""Un ejemplo de generadores"""
	print("Hello world")
	n = 0
	yield n

	print("Hello heaven")
	n = 1
	yield n

	print("Hello hell")
	n = 2
	yield n

a = my_gen()
print(next(a))
print(next(a))
print(next(a))
print(next(a)) StopIteration

Yield es lo mismo que return, con la diferencia de que yield, en lugar de terminar la función solo pausa la función hasta donde estaba ese yield. Es decir, que si después se vuelve a llamar a la función no va a comenzar desde el principio, sino desde donde se quedó el último yield.


Generator expressions

Son lo mismo que un list o dictionary comprehension pero para un generador.

my_secong_gen = (x*2 for x in my_list)

yield es una orden muy similar a un return, con una gran diferencia, yield pausará la ejecución de tu función y guardará el estado de la misma hasta que decidas usarla de nuevo.

class FiboIter():
  def __init__(self, n) -> None:
    self.n = n

  def __iter__(self):
    self.n1 = 0
    self.n2 = 1
    self.counter = 0

    return self
  
  def __next__(self):
    if self.counter == 0:
      self.counter += 1
      return self.n1
    
    elif self.counter == 1:
      self.counter += 1
      return self.n2
    
    else:
      self.aux = self.n1 + self.n2
      self.n1, self.n2 = self.n2, self.aux
      self.counter += 1

      if self.counter == self.n + 1:
        raise StopIteration
      return self.aux

Completado el reto de la clase anterior con el raise StopIteration.

Un generador que uso cuando quiero controlar timeout es time.perf_counter

Supongo que es un generador jaja porque lo instancio y luego lo llamo cada vez para contar

import time

class FiboIter():

def __init__(self, max):
    self.max = max

def __iter__(self):
    self.n1 = 0
    self.n2 = 1
    self.aux = 0
    self.counter = 0
    return self


def __next__(self):
    if not self.max or self.counter <= self.max:
    
        if self.counter == 0: 
            self.counter += 1
            return self.n1
        elif self.counter == 1:
            self.counter += 1
            return self.n2
        else: 
            self.aux = self.n1 + self.n2
            #self.n1 = self.n2
            #self.n2 = self.aux
            self.n1, self.n2 = self.n2, self.aux
            self.counter += 1 
            return self.aux
    else:
        raise StopIteration

if name == ‘main’:
max = int(input('introduzca un numero máximo de la serie: ’ ))
fibonacci = FiboIter(max)
for element in fibonacci:
print(element)
time.sleep(0.05)

Un Generador es un Iterador con Sugar Syntax, fácil de recordar.

Lo que más me llevo de la clase es el keyword yield, es genial el contenido de la saga 😄

Agregué otro pedazo de código para entender un poco mejor cómo yield recuerda el valor de la iteración anterior

def my_gem():
    'Un ejemplo de generadores'

    print('Hello World')
    n = 0
    yield n

    print('Hello Heaven')
    n = 1
    yield n

    print('Hello Hell')
    n = 2
    yield n

    print('Hello Earth')
    n = n
    yield n

a = my_gem()

print(next(a)) # Hello World
print(next(a)) # Hello Heaven
print(next(a)) # Hello Hell
print(next(a)) # Hello Earth
print(next(a)) # StopIteration

Y el Output:

Hello World
0
Hello Heaven
1
Hello Hell
2
Hello Earth
2

Qué hace el keyword "Yield" ?

Chicos les comparto un enlace para complementar la clase: https://ellibrodepython.com/yield-python

Generadores:

Una forma sencilla de generar iteradores

Interesante para ver como crece el peso en memoria a medida que se agregan más elementos a la List Comprehension y como no se afecta para nada una Generator Expression

Entendido a seguir practicando

Facundo menciona que los generadores son lo mismo que los iteradores, pero por algo les dice “sugar syntax” entre comillas, porque aunque tengan la misma funcionalidad, no son lo mismo
.
Claro que entre los dos, a menos que necesites algo muy complejo, los generadores son la opcion más rapida y eficiente

yield = coloca en suspensión la función y cuando sea llamada nuevamente ejecuta nuevamente el dato

Los generadores van sacando uno a uno los elementos

Uso de Yield

python3 generador.py
Hello world!
0
Hello heaven!
1
Hello hell!
2
Traceback (most recent call last):
  File "generador.py", line 22, in <module>
    print(next(a)) 
StopIteration