Un generador es una función que produce valores uno a uno con yield en lugar de calcularlos todos y devolverlos con return. El resultado es que el generador solo computa el siguiente valor cuando se le pide, lo que permite procesar secuencias enormes o infinitas con memoria constante.
yield: la diferencia clave con return
# Función normal: crea toda la lista en memoria
def cuadrados_lista(n):
return [x**2 for x in range(n)]
# Generador: produce un cuadrado cada vez que se le pide
def cuadrados_gen(n):
for x in range(n):
yield x**2
# Con n=1_000_000, la lista ocupa ~8 MB; el generador, ~200 bytes
import sys
lista = cuadrados_lista(1_000_000)
gen = cuadrados_gen(1_000_000)
print(sys.getsizeof(lista)) # ~8 MB
print(sys.getsizeof(gen)) # 208 bytes
Cómo consumir un generador
def contar(inicio, fin):
n = inicio
while n <= fin:
yield n
n += 1
# Iteración directa
for numero in contar(1, 5):
print(numero) # 1, 2, 3, 4, 5
# Convertir a lista (consume todo el generador)
print(list(contar(1, 5))) # [1, 2, 3, 4, 5]
# next() para pedir uno a uno
gen = contar(10, 12)
print(next(gen)) # 10
print(next(gen)) # 11
print(next(gen)) # 12
# next(gen) # StopIteration
Pipelines de generadores
Los generadores se encadenan con coste de memoria mínimo: cada elemento viaja por toda la cadena antes de pasar al siguiente:
def leer_lineas(nombre_fichero):
with open(nombre_fichero) as f:
yield from f
def filtrar_errores(lineas):
for linea in lineas:
if "ERROR" in linea:
yield linea.strip()
def limpiar(lineas):
for linea in lineas:
yield linea.lower()
# Pipeline: solo una línea en memoria a la vez
pipeline = limpiar(filtrar_errores(leer_lineas("servidor.log")))
for linea_error in pipeline:
print(linea_error)
Secuencias infinitas
def fibonacci():
a, b = 0, 1
while True: # bucle infinito: el generador nunca termina
yield a
a, b = b, a + b
# Tomar solo los primeros 10
from itertools import islice
print(list(islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
send() y throw(): comunicación bidireccional
def acumulador():
total = 0
while True:
valor = yield total # yield devuelve total y recibe el siguiente valor
if valor is None:
break
total += valor
gen = acumulador()
next(gen) # avanzar hasta el primer yield (devuelve 0)
print(gen.send(10)) # 10
print(gen.send(5)) # 15
print(gen.send(20)) # 35
yield from: delegar en otro generador
def aplanar(lista_anidada):
for elemento in lista_anidada:
if isinstance(elemento, list):
yield from aplanar(elemento) # delega recursivamente
else:
yield elemento
datos = [1, [2, 3], [4, [5, 6]], 7]
print(list(aplanar(datos))) # [1, 2, 3, 4, 5, 6, 7]
Usa generadores cuando proceses ficheros grandes, streams de datos o cualquier secuencia que no necesitas tener completa en memoria. Para secuencias finitas pequeñas donde vas a necesitar todos los valores, una lista comprehension sigue siendo más clara.
