¿Qué son los Iteradores?
Un iterador es un objeto que implementa los métodos __iter__() y __next__(), permitiendo iterar sobre una secuencia de valores uno a la vez.
Protocolo de Iteración
# Cualquier objeto iterable puede usarse en un for
lista = [1, 2, 3, 4, 5]
for elemento in lista:
print(elemento)
# Lo que realmente hace Python:
iterador = iter(lista) # Llama a __iter__()
try:
while True:
elemento = next(iterador) # Llama a __next__()
print(elemento)
except StopIteration:
pass # Fin de la iteraciónCrear un Iterador Personalizado
class Contador:
def __init__(self, limite):
self.limite = limite
self.actual = 0
def __iter__(self):
# Retorna el objeto iterador (self)
return self
def __next__(self):
if self.actual < self.limite:
self.actual += 1
return self.actual
else:
raise StopIteration
# Usar el iterador
contador = Contador(5)
for numero in contador:
print(numero) # 1, 2, 3, 4, 5¿Qué son los Generadores?
Los generadores son una forma más simple de crear iteradores usando la palabra clave yield. Son funciones que pueden pausar su ejecución y reanudarla más tarde.
Crear un Generador Simple
# Función normal que retorna lista (usa mucha memoria)
def numeros_lista(n):
resultado = []
for i in range(n):
resultado.append(i ** 2)
return resultado
# Generador (eficiente en memoria)
def numeros_generador(n):
for i in range(n):
yield i ** 2
# Usar el generador
gen = numeros_generador(5)
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 4
# O iterar con for
for numero in numeros_generador(5):
print(numero)Ventajas de los Generadores
import sys
# Lista (carga todo en memoria)
lista = [i ** 2 for i in range(1000000)]
print(f"Tamaño lista: {sys.getsizeof(lista)} bytes")
# Generador (genera valores bajo demanda)
generador = (i ** 2 for i in range(1000000))
print(f"Tamaño generador: {sys.getsizeof(generador)} bytes")
# El generador es MUCHO más pequeño en memoriaYield vs Return
# return - termina la función
def funcion_normal():
return 1
return 2 # Nunca se ejecuta
return 3
print(funcion_normal()) # 1
# yield - pausa la función
def funcion_generadora():
yield 1
yield 2
yield 3
gen = funcion_generadora()
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3Ejemplos Prácticos de Generadores
1. Leer Archivo Grande Línea por Línea
def leer_archivo_grande(nombre_archivo):
with open(nombre_archivo, 'r') as archivo:
for linea in archivo:
yield linea.strip()
# No carga todo el archivo en memoria
for linea in leer_archivo_grande('archivo_grande.txt'):
# Procesar línea por línea
if 'buscar' in linea:
print(linea)2. Secuencia de Fibonacci
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Generar primeros 10 números de Fibonacci
fib = fibonacci()
for i in range(10):
print(next(fib))
# 0, 1, 1, 2, 3, 5, 8, 13, 21, 343. Números Primos Infinitos
def numeros_primos():
def es_primo(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
n = 2
while True:
if es_primo(n):
yield n
n += 1
# Obtener primeros 20 primos
primos = numeros_primos()
for i in range(20):
print(next(primos), end=' ')
# 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 714. Procesamiento de Datos en Lotes
def procesar_en_lotes(datos, tamaño_lote):
for i in range(0, len(datos), tamaño_lote):
yield datos[i:i + tamaño_lote]
# Procesar 1 millón de registros en lotes de 1000
datos = range(1000000)
for lote in procesar_en_lotes(list(datos), 1000):
# Procesar cada lote
print(f"Procesando lote con {len(lote)} elementos")
# ... tu lógica aquíGenerator Expressions
# List comprehension - crea lista completa
cuadrados_lista = [x ** 2 for x in range(1000000)]
# Generator expression - genera bajo demanda
cuadrados_gen = (x ** 2 for x in range(1000000))
# Usar con sum (eficiente)
total = sum(x ** 2 for x in range(1000))
print(total)
# Filtrar con generator expression
pares = (x for x in range(100) if x % 2 == 0)
print(list(pares)[:10]) # Primeros 10Send() - Enviar Valores al Generador
def contador_controlado():
contador = 0
while True:
incremento = yield contador
if incremento is not None:
contador += incremento
else:
contador += 1
gen = contador_controlado()
print(next(gen)) # 0
print(next(gen)) # 1
print(gen.send(10)) # 11 (suma 10)
print(next(gen)) # 12
print(gen.send(5)) # 17 (suma 5)Generadores con Múltiples Yields
def pipeline_datos(datos):
# Etapa 1: Filtrar
print("Filtrando...")
for item in datos:
if item > 0:
yield item
def transformar(datos):
# Etapa 2: Transformar
print("Transformando...")
for item in datos:
yield item ** 2
def agregar(datos):
# Etapa 3: Agregar
print("Agregando...")
total = 0
for item in datos:
total += item
yield total
# Pipeline completo (lazy evaluation)
entrada = [-2, -1, 0, 1, 2, 3, 4, 5]
resultado = agregar(transformar(pipeline_datos(entrada)))
print(list(resultado)) # [55]Decoradores con Generadores
def contador_llamadas(func):
def wrapper(*args, **kwargs):
wrapper.llamadas += 1
print(f"Llamada #{wrapper.llamadas}")
return func(*args, **kwargs)
wrapper.llamadas = 0
return wrapper
@contador_llamadas
def numeros_pares(n):
for i in range(n):
if i % 2 == 0:
yield i
gen = numeros_pares(10)
for num in gen:
print(num)Generadores Anidados - yield from
# Sin yield from
def generador_anidado():
for i in range(3):
yield i
for j in range(3, 6):
yield j
# Con yield from (más elegante)
def generador_con_yield_from():
yield from range(3)
yield from range(3, 6)
for num in generador_con_yield_from():
print(num) # 0, 1, 2, 3, 4, 5
# Ejemplo práctico: aplanar lista anidada
def aplanar(lista):
for elemento in lista:
if isinstance(elemento, list):
yield from aplanar(elemento)
else:
yield elemento
anidada = [1, [2, 3, [4, 5]], 6, [7, [8, 9]]]
print(list(aplanar(anidada))) # [1, 2, 3, 4, 5, 6, 7, 8, 9]Itertools - Generadores Incorporados
import itertools
# count() - contador infinito
contador = itertools.count(start=10, step=2)
for i, num in enumerate(contador):
if i >= 5:
break
print(num) # 10, 12, 14, 16, 18
# cycle() - ciclo infinito
colores = itertools.cycle(['rojo', 'verde', 'azul'])
for i, color in enumerate(colores):
if i >= 7:
break
print(color) # rojo, verde, azul, rojo, verde, azul, rojo
# repeat() - repetir valor
repetir = itertools.repeat('Python', 3)
print(list(repetir)) # ['Python', 'Python', 'Python']
# chain() - encadenar iterables
lista1 = [1, 2, 3]
lista2 = [4, 5, 6]
encadenado = itertools.chain(lista1, lista2)
print(list(encadenado)) # [1, 2, 3, 4, 5, 6]
# combinations() - combinaciones
letras = ['A', 'B', 'C']
combinaciones = itertools.combinations(letras, 2)
print(list(combinaciones)) # [('A', 'B'), ('A', 'C'), ('B', 'C')]
# permutations() - permutaciones
permutaciones = itertools.permutations(['A', 'B', 'C'], 2)
print(list(permutaciones)) # [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]Caso de Uso Real: Pipeline de Datos
def leer_logs(archivo):
"""Lee archivo de logs línea por línea"""
with open(archivo, 'r') as f:
for linea in f:
yield linea.strip()
def filtrar_errores(lineas):
"""Filtra solo líneas con ERROR"""
for linea in lineas:
if 'ERROR' in linea:
yield linea
def extraer_timestamp(lineas):
"""Extrae timestamp de cada línea"""
for linea in lineas:
# Asume formato: [2025-11-17 20:00:00] ERROR ...
if linea.startswith('['):
timestamp = linea[1:20]
yield timestamp
def contar_errores_por_hora(timestamps):
"""Cuenta errores agrupados por hora"""
conteo = {}
for ts in timestamps:
hora = ts[:13] # 2025-11-17 20
conteo[hora] = conteo.get(hora, 0) + 1
return conteo
# Pipeline completo (procesa millones de líneas eficientemente)
# pipeline = leer_logs('sistema.log')
# pipeline = filtrar_errores(pipeline)
# pipeline = extraer_timestamp(pipeline)
# resultado = contar_errores_por_hora(pipeline)
# print(resultado)Comparación de Rendimiento
import time
import sys
def medir_tiempo_memoria(func, *args):
start = time.time()
resultado = func(*args)
tiempo = time.time() - start
memoria = sys.getsizeof(resultado)
return tiempo, memoria, resultado
# Con lista (eager evaluation)
def procesar_lista(n):
return [x ** 2 for x in range(n) if x % 2 == 0]
# Con generador (lazy evaluation)
def procesar_generador(n):
return (x ** 2 for x in range(n) if x % 2 == 0)
n = 1000000
# Lista
t1, m1, l = medir_tiempo_memoria(procesar_lista, n)
print(f"Lista: {t1:.4f}s, {m1} bytes")
# Generador
t2, m2, g = medir_tiempo_memoria(procesar_generador, n)
print(f"Generador: {t2:.4f}s, {m2} bytes")
# El generador usa MUCHA menos memoriaCuándo Usar Generadores
- Grandes volúmenes de datos: Cuando no necesitas todos los datos en memoria
- Secuencias infinitas: Fibonacci, números primos, etc.
- Procesamiento de archivos: Archivos grandes línea por línea
- Pipelines de datos: Transformaciones en cadena
- Búsqueda early-exit: Cuando puedes detener la búsqueda antes de procesar todo
Buenas Prácticas
- Documenta el comportamiento: Indica si el generador es infinito o finito
- Usa nombres descriptivos:
generar_fibonacci()en vez degen() - Considera el contexto: No uses generadores si necesitas indexar o iterar múltiples veces
- Aprovecha itertools: Usa las funciones integradas cuando sea posible
- Testing: Convierte a lista para testing:
list(mi_generador())
Conclusión
Los generadores e iteradores son herramientas poderosas para manejar datos eficientemente en Python. Permiten procesar grandes volúmenes de datos sin cargarlos completamente en memoria, hacer lazy evaluation, y crear secuencias infinitas elegantes. Dominar estos conceptos te convertirá en un programador Python más eficiente.