Generadores e iteradores en Python: yield, itertools y patrones eficientes
Los generadores son funciones que producen valores uno a uno, bajo demanda, sin almacenarlos todos en memoria. Son la solución ideal para procesar grandes datasets, flujos de datos o secuencias infinitas.
1. Protocolo iterador: iter y next
class Rango:
"""Iterador manual equivalente a range()."""
def __init__(self, inicio, fin, paso=1):
self.actual = inicio
self.fin = fin
self.paso = paso
def __iter__(self):
return self # El objeto mismo es el iterador
def __next__(self):
if self.actual >= self.fin:
raise StopIteration
valor = self.actual
self.actual += self.paso
return valor
for n in Rango(0, 5):
print(n, end=" ") # 0 1 2 3 4
# Uso con next() explícito
it = iter([10, 20, 30])
print(next(it)) # 10
print(next(it)) # 20
print(next(it, "fin")) # 30
print(next(it, "fin")) # fin (valor por defecto en vez de StopIteration)
2. Generadores con yield
def contar_hasta(n):
"""Genera números de 0 a n-1."""
i = 0
while i < n:
yield i
i += 1
# El cuerpo NO se ejecuta al llamar a la función
gen = contar_hasta(5)
print(type(gen)) # <class 'generator'>
for n in gen:
print(n, end=" ") # 0 1 2 3 4
# Comparación memoria: lista vs generador
import sys
lista = [i * i for i in range(100_000)]
gen = (i * i for i in range(100_000)) # Expresión generadora
print(f"Lista : {sys.getsizeof(lista):,} bytes") # ~800 KB
print(f"Generador: {sys.getsizeof(gen)} bytes") # ~200 bytes
3. Generadores para pipelines de datos
from pathlib import Path
def leer_lineas(ruta):
"""Lee un archivo línea a línea sin cargarlo en memoria."""
with open(ruta, "r", encoding="utf-8") as f:
for linea in f:
yield linea.rstrip("\n")
def filtrar_no_vacias(lineas):
for linea in lineas:
if linea.strip():
yield linea
def convertir_a_mayusculas(lineas):
for linea in lineas:
yield linea.upper()
def tomar(n, generador):
for i, item in enumerate(generador):
if i >= n:
break
yield item
# Pipeline encadenado — procesa una línea a la vez
pipeline = tomar(5,
convertir_a_mayusculas(
filtrar_no_vacias(
leer_lineas("archivo.txt")
)
)
)
for linea in pipeline:
print(linea)
4. yield from: delegar a sub-generadores
def pares(n):
for i in range(0, n, 2):
yield i
def impares(n):
for i in range(1, n, 2):
yield i
def todos(n):
yield from pares(n) # Delega al generador pares
yield from impares(n) # Luego al generador impares
print(list(todos(10)))
# [0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
# yield from con listas y otros iterables
def aplanar(estructura):
"""Aplana listas anidadas de cualquier profundidad."""
for item in estructura:
if isinstance(item, list):
yield from aplanar(item)
else:
yield item
print(list(aplanar([1, [2, [3, 4]], [5, 6], 7])))
# [1, 2, 3, 4, 5, 6, 7]
5. send() y generadores como corutinas
def acumulador():
"""Generador bidireccional: recibe y acumula valores."""
total = 0
while True:
valor = yield total # yield devuelve total Y recibe el próximo valor
if valor is None:
break
total += valor
gen = acumulador()
next(gen) # Inicializar (avanzar hasta el primer yield)
print(gen.send(10)) # 10
print(gen.send(20)) # 30
print(gen.send(15)) # 45
6. itertools: herramientas de iteración estándar
import itertools
# count: contador infinito
for n in itertools.islice(itertools.count(start=1, step=2), 5):
print(n, end=" ") # 1 3 5 7 9
print()
# cycle: cicla sobre un iterable indefinidamente
colores = ["rojo", "verde", "azul"]
for i, color in enumerate(itertools.cycle(colores)):
if i >= 6:
break
print(color, end=" ") # rojo verde azul rojo verde azul
print()
# chain: encadenar múltiples iterables
resultado = list(itertools.chain([1, 2], [3, 4], [5, 6]))
print(resultado) # [1, 2, 3, 4, 5, 6]
# islice: rebanada sin lista
primeros_10 = list(itertools.islice(itertools.count(), 10))
print(primeros_10) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# takewhile y dropwhile
datos = [1, 3, 5, 2, 4, 6, 8]
print(list(itertools.takewhile(lambda x: x < 5, datos))) # [1, 3]
print(list(itertools.dropwhile(lambda x: x < 5, datos))) # [2, 4, 6, 8]
7. itertools: combinatorias y agrupamiento
import itertools
# product: producto cartesiano
for combo in itertools.product("AB", repeat=2):
print("".join(combo), end=" ") # AA AB BA BB
print()
# permutations y combinations
items = ["a", "b", "c"]
print(list(itertools.permutations(items, 2)))
# [('a','b'),('a','c'),('b','a'),('b','c'),('c','a'),('c','b')]
print(list(itertools.combinations(items, 2)))
# [('a','b'),('a','c'),('b','c')]
print(list(itertools.combinations_with_replacement(items, 2)))
# [('a','a'),('a','b'),('a','c'),('b','b'),('b','c'),('c','c')]
# groupby: agrupar por clave (requiere datos ordenados)
datos = [
{"tipo": "imagen", "nombre": "foto.jpg"},
{"tipo": "imagen", "nombre": "logo.png"},
{"tipo": "video", "nombre": "clip.mp4"},
{"tipo": "imagen", "nombre": "banner.webp"},
]
datos.sort(key=lambda d: d["tipo"])
for tipo, grupo in itertools.groupby(datos, key=lambda d: d["tipo"]):
archivos = [d["nombre"] for d in grupo]
print(f"{tipo}: {archivos}")
# imagen: ['foto.jpg', 'logo.png', 'banner.webp']
# video: ['clip.mp4']
8. Generadores infinitos útiles
import itertools
def fibonacci():
"""Secuencia de Fibonacci infinita."""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Primeros 10 números de Fibonacci
print(list(itertools.islice(fibonacci(), 10)))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
def potencias_de_2():
n = 1
while True:
yield n
n *= 2
# Todas las potencias de 2 menores que 1000
for p in itertools.takewhile(lambda x: x < 1000, potencias_de_2()):
print(p, end=" ") # 1 2 4 8 16 32 64 128 256 512
9. Procesar archivos CSV grandes con generadores
import csv
from pathlib import Path
def leer_csv(ruta, encoding="utf-8"):
"""Lee un CSV grande línea a línea."""
with open(ruta, newline="", encoding=encoding) as f:
reader = csv.DictReader(f)
for fila in reader:
yield fila
def filtrar_por_campo(filas, campo, valor):
for fila in filas:
if fila.get(campo) == valor:
yield fila
def transformar(filas):
for fila in filas:
fila["precio"] = float(fila.get("precio", 0))
yield fila
# Pipeline completo: CSV de 5 GB procesado fila a fila
pipeline = transformar(
filtrar_por_campo(
leer_csv("ventas_grandes.csv"),
campo="categoria",
valor="electronica",
)
)
total = 0
for fila in pipeline:
total += fila["precio"]
print(f"Total electrónica: {total:.2f}")
10. Buenas prácticas
- Usa generadores cuando el dataset es grande o potencialmente infinito.
- Expresión generadora
(x for x in ...)vs lista[x for x in ...]: la primera es lazy, la segunda almacena todo. itertools.islicepara truncar generadores infinitos sin bucles manuales.yield fromsimplifica la recursividad y la delegación a sub-generadores.- Un generador solo se puede consumir una vez: si necesitas iterarlo de nuevo, crea uno nuevo.
itertools.groupbyrequiere que los datos estén ordenados por la misma clave que se agrupa.
Resumen de itertools más usados
| Función | Descripción |
|---|---|
count(n) |
Contador infinito desde n |
cycle(it) |
Cicla infinitamente sobre el iterable |
islice(it, n) |
Tomar los primeros n elementos |
chain(*its) |
Concatenar iterables |
takewhile(f, it) |
Tomar mientras se cumple la condición |
dropwhile(f, it) |
Saltar mientras se cumple la condición |
groupby(it, key) |
Agrupar por clave |
product(*its) |
Producto cartesiano |
combinations(it, r) |
Combinaciones sin repetición |
permutations(it, r) |
Permutaciones |
Conversiones relacionadas
Conversiones frecuentes del catálogo: