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
Introducción
¿Qué necesitas saber para tomar el curso?
¿Cómo funciona Python?
Cómo organizar las carpetas de tus proyectos
Static Typing
¿Qué son los tipados?
Tipado estático en Python
Practicando el tipado estático
Conceptos avanzados de funciones
Scope: alcance de las variables
Closures
Programando closures
Decoradores
Programando decoradores
Estructuras de datos avanzadas
Iteradores
La sucesión de Fibonacci
Generadores
Mejorando nuestra sucesión de Fibonacci
Sets
Operaciones con sets
Eliminando los repetidos de una lista
Bonus
Manejo de fechas
Time zones
Conclusión
Completaste la trilogía. ¿Cómo seguir?
Aún no tienes acceso a esta clase
Crea una cuenta y continúa viendo este curso
Aportes 40
Preguntas 10
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
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.
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:
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
.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
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:
¡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.
Los GENERADORES son un “Sugar syntax” de los iteradores. Los generadores son funciones que guardan 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
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)
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())
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))
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))
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))
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)
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)
Y su equivalente con generadores sería lo siguiente.
al_cuadrado_generador = (x**2 for x in lista)
print(al_cuadrado_generador)
Y como hemos visto los valores pueden ser generados de la siguiente forma.
for i in al_cuadrado_generador:
print(i)
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)))
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)))
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)))
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.
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
¿Quieres ver más aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesión.