Si alguna vez has escrito un bucle for en Python, ya has usado iteradores — incluso sin saberlo. Pero cuando necesitas procesar grandes volúmenes de datos, crear flujos infinitos o simplemente escribir código más elegante, entender los generadores e iteradores se vuelve esencial.
En esta guía definitiva, aprenderás qué son los iteradores y generadores, cómo funcionan internamente y cómo usarlos para transformar la forma en que escribes Python. Con ejemplos prácticos, verás cómo estas herramientas pueden reducir el consumo de memoria, simplificar tu código y abrirte las puertas a patrones de programación más avanzados.
¿Qué Son los Iteradores?
En Python, un iterador es un objeto que implementa el protocolo de iteración: los métodos __iter__() y __next__(). El primero devuelve el propio iterador, y el segundo devuelve el siguiente elemento de la secuencia. Cuando no hay más elementos, __next__() lanza la excepción StopIteration.
Prácticamente todas las estructuras de datos en Python son iterables: listas, tuplas, diccionarios, conjuntos, cadenas. Cuando escribes for item in lista, Python internamente llama a iter(lista) para obtener un iterador y luego llama a next() en cada iteración.
lista = [10, 20, 30]
iterador = iter(lista)
print(next(iterador)) # 10
print(next(iterador)) # 20
print(next(iterador)) # 30
# print(next(iterador)) # StopIteration
Entender este mecanismo es el primer paso para crear tus propios iteradores personalizados y, más importante aún, para dominar los generadores — la forma más elegante de crear iteradores en Python.
El Protocolo de Iteración
Cualquier objeto que tenga los métodos __iter__ y __next__ se considera un iterador. El método __iter__ debe devolver el propio objeto iterador, y __next__ debe devolver el siguiente valor o lanzar StopIteration.
class Contador:
def __init__(self, limite):
self.limite = limite
self.actual = 0
def __iter__(self):
return self
def __next__(self):
if self.actual >= self.limite:
raise StopIteration
valor = self.actual
self.actual += 1
return valor
for numero in Contador(5):
print(numero) # 0, 1, 2, 3, 4
Aunque funciona perfectamente, crear iteradores con clases es verboso. Aquí es donde entran los generadores.
¿Qué Son los Generadores?
Un generador es una función especial que usa la palabra clave yield en lugar de return. A diferencia de una función normal que se ejecuta y termina, una función generadora mantiene su estado entre llamadas — se "pausa" en cada yield y se reanuda desde donde se quedó en la siguiente llamada.
Cuando llamas a una función generadora, no se ejecuta inmediatamente. En su lugar, devuelve un objeto generador (que es un iterador). Cada vez que llamas a next() sobre él, la función se ejecuta hasta el siguiente yield y devuelve ese valor.
def contador_simple():
print("Comenzando...")
yield 1
print("Continuando...")
yield 2
print("Finalizando...")
yield 3
gen = contador_simple()
print(next(gen)) # Comenzando... 1
print(next(gen)) # Continuando... 2
print(next(gen)) # Finalizando... 3
Esa es la magia de los generadores: permiten la evaluación perezosa (lazy evaluation), es decir, los valores se computan bajo demanda, solo cuando se necesitan.
Yield vs Return: ¿Cuál es la Diferencia?
La diferencia fundamental entre yield y return es el comportamiento del estado:
return: termina la función permanentemente y devuelve un valor.yield: pausa la función, guarda todo su estado (variables locales, posición en el código) y devuelve un valor. En la siguiente llamada, la función se reanuda exactamente donde se quedó.
Una función generadora puede tener múltiples yield e incluso puede mezclar yield con return. Cuando se usa return en un generador, termina la iteración lanzando StopIteration, y el valor pasado a return está disponible a través de la excepción (aunque esto rara vez se usa).
Para un análisis técnico completo, consulta la PEP 255 — Simple Generators, que introdujo oficialmente los generadores en Python 2.2.
Generator Expressions
Así como tienes list comprehensions para crear listas de forma concisa, existen las generator expressions para crear generadores. La sintaxis es casi idéntica, pero usa paréntesis en lugar de corchetes.
# List comprehension — crea una lista completa en memoria
cuadrados_lista = [x**2 for x in range(1000000)] # ~ 8 MB de memoria
# Generator expression — crea un generador perezoso
cuadrados_gen = (x**2 for x in range(1000000)) # ~ 56 bytes de memoria
print(next(cuadrados_gen)) # 0
print(next(cuadrados_gen)) # 1
print(next(cuadrados_gen)) # 4
La diferencia de memoria es astronómica: la list comprehension asigna espacio para un millón de enteros de una sola vez, mientras que la generator expression crea un generador que ocupa unos pocos bytes y calcula cada valor bajo demanda.
Las generator expressions se introdujeron en Python 2.4 mediante la PEP 289 — Generator Expressions, que detalla la motivación y el diseño de esta característica.
Si quieres entender mejor las list comprehensions antes de migrar a generator expressions, consulta nuestra guía completa: List Comprehension en Python: Guía Completa.
La Expresión Yield From
Introducida en Python 3.3 (PEP 380), yield from permite que un generador delegue iteraciones a otro generador o iterable. Esto simplifica la composición de generadores y evita anidamientos verbosos.
def generador_principal():
yield from [1, 2, 3]
yield from range(4, 7)
yield from "ab"
for valor in generador_principal():
print(valor, end=" ") # 1 2 3 4 5 6 a b
Sin yield from, tendrías que iterar manualmente sobre cada sub-iterador:
def generador_sin_yield_from():
for item in [1, 2, 3]:
yield item
for item in range(4, 7):
yield item
for item in "ab":
yield item
La expresión yield from también maneja automáticamente la comunicación bidireccional entre generadores — los valores enviados con send() se propagan al generador delegado, y las excepciones se transmiten correctamente.
Generadores Infinitos
Una de las aplicaciones más impresionantes de los generadores es la creación de secuencias infinitas. Como los valores se computan bajo demanda, puedes representar secuencias teóricamente infinitas sin consumir memoria infinita.
def fibonacci_infinito():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci_infinito()
for _ in range(10):
print(next(fib), end=" ") # 0 1 1 2 3 5 8 13 21 34
Con generadores infinitos, controlas exactamente cuántos valores consumir. El generador no sabe que es "infinito" — simplemente sigue produciendo valores mientras sigas llamando a next().
El módulo itertools de la biblioteca estándar ofrece diversas herramientas para trabajar con generadores e iteradores, incluyendo funciones como islice() que permite "rebanar" generadores infinitos.
Aplicaciones Prácticas
1. Procesamiento de Archivos Grandes
Los generadores brillan cuando necesitas procesar archivos más grandes que la memoria disponible. Un generador puede leer y procesar el archivo línea por línea:
def lineas_archivo(ruta):
with open(ruta, 'r', encoding='utf-8') as archivo:
for linea in archivo:
yield linea.strip()
def lineas_con_palabra(lineas, palabra):
for linea in lineas:
if palabra in linea:
yield linea
def contar_ocurrencias(lineas, palabra):
return sum(1 for linea in lineas if palabra in linea)
# Uso encadenado — memoria constante
total = contar_ocurrencias(lineas_archivo("datos.csv"), "Python")
print(f"Ocurrencias: {total}")
Cada generador hace una sola cosa y la hace bien. El encadenamiento mantiene el uso de memoria constante — en ningún momento todo el archivo está en la RAM.
2. Pipelines de Datos
Los generadores son ideales para construir pipelines de procesamiento donde cada etapa es un generador independiente:
def leer_sensores():
import random
while True:
yield random.uniform(20, 30)
def filtrar_valores(datos, minimo, maximo):
for valor in datos:
if minimo <= valor <= maximo:
yield valor
def normalizar(datos, minimo, maximo):
for valor in datos:
yield (valor - minimo) / (maximo - minimo)
pipeline = normalizar(
filtrar_valores(leer_sensores(), 22, 28),
22, 28
)
for _ in range(5):
print(f"{next(pipeline):.3f}")
3. Evaluación Perezosa en Data Science
En ciencia de datos, los generadores evitan cargar conjuntos de datos completos en memoria. Bibliotecas como pandas se integran bien con generadores para el procesamiento de datos en streaming.
def lotes(datos, tamano):
"""Divide un iterable en lotes de tamaño fijo."""
lote = []
for item in datos:
lote.append(item)
if len(lote) == tamano:
yield lote
lote = []
if lote:
yield lote
datos = range(100)
for i, lote in enumerate(lotes(datos, 10)):
print(f"Lote {i}: {list(lote)}")
if i >= 3:
break
Iteración Bidireccional con send()
Los generadores no son solo productores unidireccionales de datos. El método send() permite enviar valores de vuelta al generador, que los recibe como el valor de la expresión yield. Esto convierte a los generadores en corutinas simples.
def corrector():
print("Esperando corrección...")
while True:
valor = yield
print(f"Corrigiendo: {valor ** 2}")
c = corrector()
next(c) # Inicializa el generador
c.send(5) # Corrigiendo: 25
c.send(10) # Corrigiendo: 100
Aunque async/await ha reemplazado este patrón para corutinas modernas, entender send() es útil para depuración y para patrones específicos de comunicación entre generadores.
Comparación: Generadores vs Listas vs Iteradores
| Característica | Lista | Iterador (clase) | Generador |
|---|---|---|---|
| Memoria | Alta (todo en RAM) | Baja | Bajísima |
| Reiterable | Sí | No (un solo uso) | No (un solo uso) |
| Acceso aleatorio | Sí (índices) | No | No |
| Sintaxis | [...] |
Verbosa | Concisa |
| Lazy | No | Sí | Sí |
| Puede ser infinito | No | Sí | Sí |
Cada enfoque tiene su lugar. Las listas son excelentes para colecciones pequeñas o cuando necesitas acceso aleatorio. Los iteradores son apropiados cuando necesitas control detallado. Los generadores son la elección ideal para la mayoría de los casos de procesamiento secuencial, combinando simplicidad con eficiencia.
Generadores en la Práctica: Ejemplo de Web Scraping
Veamos un ejemplo práctico que combina varios conceptos. Supón que necesitas extraer múltiples páginas de un sitio web y extraer información:
import time
def paginas_api(base_url, total_paginas):
"""Genera URLs de las páginas bajo demanda."""
for i in range(1, total_paginas + 1):
yield f"{base_url}?pagina={i}"
def descargar_paginas(urls):
"""Simula descarga (reemplaza con aiohttp o httpx)."""
for url in urls:
time.sleep(0.5) # Simula latencia
yield f"Datos de {url}"
def extraer_datos(respuestas):
"""Extrae información relevante de cada respuesta."""
for respuesta in respuestas:
yield {
"fuente": respuesta,
"items": len(respuesta),
"timestamp": time.time()
}
urls = paginas_api("https://api.ejemplo.com/datos", 100)
respuestas = descargar_paginas(urls)
datos = extraer_datos(respuestas)
# Consume solo 20 páginas, sin desperdicio
for i, item in enumerate(datos):
if i >= 20:
break
print(f"{item['fuente']} - {item['items']} items")
Este patrón de pipeline con generadores mantiene el código limpio, modular y eficiente. Cada etapa se puede probar de forma independiente y el consumo de memoria es mínimo.
Si trabajas con web scraping, consulta también nuestro artículo sobre Lambda, Map, Filter y Reduce en Python — funciones que se integran perfectamente con generadores.
Consejos y Buenas Prácticas
1. Prefiere Generadores a Listas Grandes
Siempre que no necesites acceder a los datos más de una vez o necesites acceso aleatorio, prefiere generadores en lugar de listas. La diferencia de rendimiento y memoria es significativa, especialmente con grandes volúmenes.
2. Usa itertools para Composición
El módulo itertools ofrece funciones como chain(), zip_longest(), islice(), takewhile() y dropwhile() que se componen perfectamente con generadores. Combinar itertools con generator expressions es una de las formas más expresivas de escribir Python.
3. Cuidado con los Efectos Secundarios
Los generadores son ideales para computación pura — no para operaciones con efectos secundarios como escribir archivos o modificar variables globales. Si necesitas efectos secundarios, sé explícito y documéntalos.
4. No Te Excedas
Para colecciones pequeñas (unas pocas decenas de elementos), una lista simple es perfectamente aceptable. La optimización prematura es la raíz de todos los males — usa generadores cuando tenga sentido, no por dogma.
Referencias y Profundización
Para continuar tus estudios sobre generadores e iteradores, consulta estas fuentes confiables:
- Documentación Oficial — Iteradores
- Documentación Oficial — Generadores
- PEP 255 — Simple Generators
- PEP 289 — Generator Expressions
- Real Python: Introduction to Python Generators
- Python Wiki: Generators
- itertools — Biblioteca Estándar
- Yield Expression — Referencia del Lenguaje
Conclusión
Los generadores e iteradores son herramientas fundamentales en el arsenal de cualquier desarrollador Python. Permiten escribir código más limpio, más eficiente y más expresivo, especialmente cuando se manejan grandes volúmenes de datos o flujos continuos.
El secreto está en la evaluación perezosa: computar solo lo que se necesita, cuando se necesita. Esto no solo ahorra memoria sino que también hace que el código sea más modular y comprobable.
Ahora que dominas estos conceptos, ¡practica! Crea tus propios generadores, experimenta con yield from, explora el módulo itertools y descubre cómo estas herramientas pueden transformar la calidad de tu código Python.
Para más contenido sobre Python, sigue explorando Universo Python!