4.1 Introducción a los generadores
Generadores son funciones que devuelven un objeto iterador. Estos iteradores generan valores según se solicitan, lo que permite manejar conjuntos de datos potencialmente grandes sin cargarlos completamente en la memoria.
Hay varias formas de crear generadores, a continuación se discutirán las más populares.
Generadores basados en funciones
Los generadores se crean usando la palabra clave yield dentro de una función. Cuando se llama una función con yield, devuelve un objeto generador, pero no ejecuta el código dentro de la función inmediatamente. En su lugar, la ejecución se suspende en la expresión yield y se reanuda con cada llamada al método __next__() del objeto generador.
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1
counter = count_up_to(5)
print(next(counter)) # Salida: 1
print(next(counter)) # Salida: 2
print(next(counter)) # Salida: 3
print(next(counter)) # Salida: 4
print(next(counter)) # Salida: 5
Si hay una declaración yield en la función, Python en lugar de la ejecución tradicional de la función crea un objeto generador que gestiona el estado de ejecución de la función.
Expresiones generadoras
Las expresiones generadoras son similares a las List Comprehension, pero se crean usando paréntesis en lugar de corchetes. También devuelven un objeto generador.
squares = (x ** 2 for x in range(10))
print(next(squares)) # Salida: 0
print(next(squares)) # Salida: 1
print(next(squares)) # Salida: 4
¿Cuál de las formas te gusta más?
3.2 Ventajas de los generadores
Uso eficiente de memoria
Los generadores calculan valores sobre la marcha, lo que permite manejar grandes datos sin cargarlos completamente en la memoria. Esto hace que los generadores sean una opción ideal para trabajar con grandes conjuntos de datos o flujos de datos.
def large_range(n):
for i in range(n):
yield i
for value in large_range(1000000):
# Procesamos los valores uno por uno
print(value)
Cálculo perezoso
Los generadores realizan cálculos perezosos, es decir, calculan valores solo cuando es necesario. Esto permite evitar cálculos innecesarios y mejora el rendimiento.
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib))
Conveniencia sintáctica
Los generadores proporcionan una sintaxis conveniente para crear iteradores, lo que simplifica la escritura y lectura del código.
3.3 Uso de generadores
Ejemplos de uso de generadores en la biblioteca estándar
Muchas funciones en la biblioteca estándar de Python utilizan generadores. Por ejemplo, la función range() devuelve un objeto generador, que genera una secuencia de números.
for i in range(10):
print(i)
Sí, el mundo ya nunca será el mismo.
Creación de secuencias infinitas
Los generadores permiten crear secuencias infinitas, que pueden ser útiles en varios escenarios, como la generación de flujos de datos infinitos.
def natural_numbers():
n = 1
while True:
yield n
n += 1
naturals = natural_numbers()
for _ in range(10):
print(next(naturals))
Uso de send() y close()
Los objetos generadores soportan los métodos send() y close(), que permiten enviar valores de vuelta al generador y terminar su ejecución.
def echo():
while True:
received = yield
print(received)
e = echo()
next(e) # Iniciamos el generador
e.send("Hello, world!") # Salida: Hello, world!
e.close()
3.4 Generadores en la práctica
Generadores y excepciones
Los generadores pueden manejar excepciones, lo que los convierte en una herramienta poderosa para escribir código más resistente.
def controlled_execution():
try:
yield "Start"
yield "Working"
except GeneratorExit:
print("Generator closed")
gen = controlled_execution()
print(next(gen)) # Salida: Start
print(next(gen)) # Salida: Working
gen.close() # Salida: Generator closed
Veremos el manejo de excepciones en las próximas lecciones, pero creo que será útil que sepas que los generadores funcionan bien con ellas.
Generadores anidados
Los generadores pueden ser anidados, lo que permite crear estructuras iterativas complejas.
def generator1():
yield from range(3)
yield from "ABC"
for value in generator1():
print(value)
# Salida
0
1
2
A
B
C
Explicación:
yield from: Esta construcción se utiliza para delegar parte de las operaciones a otro generador, lo que permite simplificar el código y mejorar la legibilidad.
Generadores y rendimiento
El uso de generadores puede mejorar significativamente el rendimiento de los programas al reducir el uso de memoria y efectuar iteraciones de manera más eficiente.
Ejemplo de comparación entre listas y generadores
import time
import sys
def memory_usage(obj):
return sys.getsizeof(obj)
n = 10_000_000
# Uso de lista
start_time = time.time()
list_comp = [x ** 2 for x in range(n)]
list_time = time.time() - start_time
list_memory = memory_usage(list_comp)
# Uso de generador
start_time = time.time()
gen_comp = (x ** 2 for x in range(n))
gen_result = sum(gen_comp) # Calculamos la suma para comparar resultados
gen_time = time.time() - start_time
gen_memory = memory_usage(gen_comp)
print(f"Lista:")
print(f" Tiempo: {list_time:.2f} seg")
print(f" Memoria: {list_memory:,} bytes")
print(f"\nGenerador:")
print(f" Tiempo: {gen_time:.2f} seg")
print(f" Memoria: {gen_memory:,} bytes")
Lista:
Tiempo: 0.62 seg
Memoria: 89,095,160 bytes
Generador:
Tiempo: 1.13 seg
Memoria: 200 bytes
GO TO FULL VERSION