Iteradores y generadores en Python
En esta lección
Llevas tiempo usando for para recorrer listas, cadenas, diccionarios y rangos. Pero ¿cómo sabe for recorrer cosas tan distintas? La respuesta son los iteradores, un mecanismo muy sencillo que hay detrás de cada bucle. En esta lección verás cómo funcionan, cómo crear los tuyos y, sobre todo, cómo escribir generadores con yield: funciones que producen valores de uno en uno, sin guardarlos todos en memoria. Son la herramienta de Python para trabajar con datos muy grandes o incluso infinitos.
Iterables e iteradores
Hay dos palabras parecidas que conviene separar bien:
- Un iterable es cualquier cosa que se puede recorrer con
for: listas, tuplas, cadenas, diccionarios, conjuntos,range, ficheros… - Un iterador es el objeto que hace el recorrido: sabe cuál es el siguiente elemento y recuerda por dónde va.
Piensa en un libro y un marcapáginas. El libro es el iterable: tiene todas las páginas. El marcapáginas es el iterador: indica por qué página vas. Puedes tener varios marcapáginas en el mismo libro, cada uno en una página distinta.
iter() y next()
La función iter() pide a un iterable un iterador nuevo, y next() le pide al iterador el siguiente elemento:
dias = ["lunes", "martes", "miércoles"]
marcador = iter(dias)
print(next(marcador)) # lunes
print(next(marcador)) # martes
print(next(marcador)) # miércoles
¿Y si pides uno más? El iterador avisa de que se ha terminado lanzando la excepción StopIteration:
try:
next(marcador)
except StopIteration:
print("No quedan elementos")
# No quedan elementos
next() admite un segundo argumento con el valor que devolverá cuando no queden elementos, en lugar de lanzar la excepción:
print(next(marcador, "fin")) # fin
Un iterador solo avanza: no puede volver atrás ni empezar de nuevo. Cuando se agota, se queda agotado. Si quieres recorrer otra vez, pide un iterador nuevo con iter(dias).
Lo que hace for por dentro
Ahora puedes entender qué hace un bucle for. Este código:
for dia in dias:
print(dia.upper())
# LUNES
# MARTES
# MIÉRCOLES
es, por dentro, equivalente a este:
iterador = iter(dias)
while True:
try:
dia = next(iterador)
except StopIteration:
break
print(dia.upper())
# LUNES
# MARTES
# MIÉRCOLES
for llama a iter() una vez, después llama a next() en cada vuelta y termina cuando recibe StopIteration. Por eso funciona con cualquier objeto que sepa dar un iterador, sea del tipo que sea.
Crear tu propio iterador
Para que un objeto sea un iterador necesita dos métodos especiales:
__iter__(): devuelve el iterador. En un iterador, se devuelve a sí mismo (return self).__next__(): devuelve el siguiente valor o lanzaStopIterationcuando no quedan.
Vamos a crear un iterador que recorra los kilómetros de una ruta de senderismo de cinco en cinco:
class PuntosDeControl:
def __init__(self, distancia_total, cada=5):
self.distancia_total = distancia_total
self.cada = cada
self.actual = 0
def __iter__(self):
return self
def __next__(self):
if self.actual > self.distancia_total:
raise StopIteration
km = self.actual
self.actual += self.cada
return km
for km in PuntosDeControl(18):
print(f"Control en el km {km}")
# Control en el km 0
# Control en el km 5
# Control en el km 10
# Control en el km 15
Funciona, pero hay que escribir bastante: una clase, dos métodos y llevar el estado (self.actual) a mano. Además, como el objeto es su propio iterador, solo se puede recorrer una vez:
ruta = PuntosDeControl(10)
print(list(ruta)) # [0, 5, 10]
print(list(ruta)) # [] (ya está agotado)
Por suerte, casi nunca tendrás que escribir iteradores así. Python tiene una forma mucho más cómoda: los generadores.
Generadores con yield
Un generador es una función que, en lugar de return, usa yield (producir, ceder). Cada yield entrega un valor y pausa la función justo en ese punto. Cuando se pide el siguiente valor, la función continúa donde se quedó, con todas sus variables intactas.
El mismo iterador de antes, como generador:
def puntos_de_control(distancia_total, cada=5):
km = 0
while km <= distancia_total:
yield km
km += cada
for km in puntos_de_control(18):
print(f"Control en el km {km}")
# Control en el km 0
# Control en el km 5
# Control en el km 10
# Control en el km 15
Cuatro líneas en lugar de una clase entera. Python crea por ti __iter__, __next__ y el StopIteration final, que se lanza solo cuando la función termina.
Paso a paso
Llamar a una función generadora no ejecuta su código: devuelve un objeto generador, que es un iterador. El código avanza cada vez que llamas a next():
def preparar_cafe():
print(" (moliendo café)")
yield "café molido"
print(" (calentando agua)")
yield "agua caliente"
print(" (sirviendo)")
yield "café listo"
cafetera = preparar_cafe()
print(type(cafetera).__name__) # generator
print(next(cafetera))
print(next(cafetera))
print(next(cafetera))
# generator
# (moliendo café)
# café molido
# (calentando agua)
# agua caliente
# (sirviendo)
# café listo
Fíjate en el orden: el primer print de la función no aparece hasta el primer next(). Entre un next() y otro, la función está “congelada” esperando.
yield frente a return
return | yield | |
|---|---|---|
| Qué devuelve | Un valor y la función termina | Un valor y la función se pausa |
| Cuántas veces | Una | Tantas como quieras |
| Resultado de llamar a la función | El valor | Un generador |
| Memoria | Todo el resultado de golpe | Un valor cada vez |
Un generador también puede tener return (sin valor): sirve para terminar antes de tiempo.
Evaluación perezosa: ahorrar memoria
Los generadores son perezosos: no calculan un valor hasta que alguien lo pide. Esto tiene una consecuencia enorme en el uso de memoria. Compara una lista de un millón de números con un generador que produce los mismos números:
import sys
lista = [n * n for n in range(1_000_000)]
generador = (n * n for n in range(1_000_000))
print(sys.getsizeof(lista) > 1_000_000) # True: varios MB
print(sys.getsizeof(generador) < 500) # True: unos pocos bytes
print(sum(generador)) # 333332833333500000
sys.getsizeof da el tamaño de un objeto en bytes (el número exacto depende de la versión de Python). La lista guarda el millón de resultados; el generador solo guarda cómo calcular el siguiente. sum va pidiéndole valores de uno en uno y nunca hay más de uno en memoria.
Esto es lo que hace Python al leer un fichero línea a línea con for linea in fichero: no carga el fichero entero, sino una línea cada vez. Así puedes procesar ficheros de varios gigas.
Generadores infinitos
Como solo se calcula lo que se pide, un generador puede no terminar nunca. Por ejemplo, números de ticket para la cola de una carnicería:
def turnos(prefijo):
numero = 1
while True:
yield f"{prefijo}{numero:03d}"
numero += 1
cola = turnos("C")
print(next(cola), next(cola), next(cola)) # C001 C002 C003
El while True no bloquea el programa, porque la función se pausa en cada yield. Eso sí: nunca hagas list(turnos("C")), porque intentaría crear una lista infinita.
Expresiones generadoras
Ya viste una arriba: (n * n for n in range(...)). Una expresión generadora se escribe igual que una comprensión de lista, pero con paréntesis en lugar de corchetes. Crea un generador sin necesidad de escribir una función:
precios = [12.5, 3.99, 45.0, 7.25, 19.9]
caros = (p for p in precios if p > 10)
print(caros) # <generator object <genexpr> at 0x...>
print(list(caros)) # [12.5, 45.0, 19.9]
print(round(sum(p * 1.21 for p in precios), 2)) # 107.25
print(max(len(d) for d in dias)) # 9
print(any(p > 40 for p in precios)) # True
Cuando la expresión es el único argumento de una función, como en sum(...), puedes omitir los paréntesis extra. Úsalas cuando vayas a recorrer los datos una sola vez (sumar, buscar el máximo, comprobar con any o all): ahorran memoria y no crean una lista que nadie va a usar.
Cuidado: la dirección que muestra
print(caros)(0x...) cambia en cada ejecución. Y recuerda que un generador se agota: después delist(caros), el generadorcarosya está vacío.
Encadenar generadores: tuberías de datos
Como cada generador consume otro iterable, puedes conectarlos como piezas de una tubería. Cada paso procesa un elemento y se lo pasa al siguiente:
registro = [
"2026-10-01;Ana;45",
"2026-10-01;Luis;error",
"",
"2026-10-02;Marta;60",
"2026-10-03;Ana;30",
]
def lineas_validas(lineas):
for linea in lineas:
if linea.strip():
yield linea
def separar(lineas):
for linea in lineas:
fecha, nombre, minutos = linea.split(";")
if minutos.isdigit():
yield nombre, int(minutos)
entrenos = separar(lineas_validas(registro))
for nombre, minutos in entrenos:
print(f"{nombre:<6} {minutos:>3} min")
# Ana 45 min
# Marta 60 min
# Ana 30 min
Si registro fuera un fichero de millones de líneas, el programa usaría la misma memoria: cada línea atraviesa la tubería entera antes de leer la siguiente.
yield from: delegar en otro iterable
Cuando un generador quiere producir todos los valores de otro iterable, puede usar yield from en lugar de un bucle:
def todas_las_tareas(proyectos):
for tareas in proyectos.values():
yield from tareas # equivale a: for t in tareas: yield t
proyectos = {
"web": ["maquetar", "formulario"],
"app": ["login", "notificaciones", "pruebas"],
}
print(list(todas_las_tareas(proyectos)))
# ['maquetar', 'formulario', 'login', 'notificaciones', 'pruebas']
Es especialmente útil en generadores recursivos, como uno que recorre una lista anidada a cualquier profundidad (repasa la recursividad si lo necesitas):
def aplanar(datos):
for elemento in datos:
if isinstance(elemento, list):
yield from aplanar(elemento)
else:
yield elemento
print(list(aplanar([1, [2, [3, 4]], 5]))) # [1, 2, 3, 4, 5]
itertools: generadores ya hechos
El módulo itertools de la biblioteca estándar trae muchos iteradores listos para usar. Tres de los más útiles:
from itertools import count, islice, chain
# count: cuenta sin fin desde un número
for n in count(10, 5):
if n > 25:
break
print(n, end=" ")
print()
# 10 15 20 25
# islice: corta un iterador como si fuera una lista (sin crearla)
print(list(islice(turnos("P"), 3))) # ['P001', 'P002', 'P003']
print(list(islice(count(), 2, 10, 3))) # [2, 5, 8]
# chain: encadena varios iterables como si fueran uno
mananas = ["yoga", "pilates"]
tardes = ["escalada", "boulder"]
for clase in chain(mananas, tardes):
print(clase, end=" | ")
print()
# yoga | pilates | escalada | boulder |
islice es la forma segura de sacar unos pocos elementos de un generador infinito, porque a los generadores no se les puede aplicar [:3]. Otras funciones prácticas son zip y enumerate, que ya conoces y que también son iteradores perezosos.
Cuándo usar cada cosa
| Necesitas… | Usa |
|---|---|
| Recorrer los datos varias veces o acceder por posición | Una lista |
| Recorrer una vez, sobre todo con muchos datos | Una expresión generadora |
| Una secuencia con lógica (estado, condiciones, varios pasos) | Una función con yield |
| Un objeto con más métodos que, además, se pueda recorrer | Una clase con __iter__ (que puede ser un generador) |
| Combinar, cortar o repetir iterables | itertools |
Errores frecuentes
- Recorrer un generador dos veces: la segunda vez no da nada, porque ya está agotado. Si necesitas los datos varias veces, guárdalos en una lista con
list(...). - Esperar que la función generadora se ejecute al llamarla:
preparar_cafe()no imprime nada; solo crea el generador. El código corre al pedir valores connext()ofor. - Usar
len()o índices con un generador:len(gen)ygen[0]danTypeError. Los generadores no saben cuántos elementos tienen ni permiten saltar a una posición. - Convertir en lista un generador infinito:
list(count())no termina nunca. Usaisliceo unbreak. - Confundir corchetes y paréntesis:
[x for x in datos]crea una lista;(x for x in datos), un generador. - Olvidar
StopIterationen__next__: un iterador escrito a mano sin esa excepción produce valores para siempre.
Resumen
| Concepto | Qué es | Ejemplo |
|---|---|---|
| Iterable | Se puede recorrer con for | Lista, cadena, range, fichero |
| Iterador | Da el siguiente elemento y recuerda la posición | it = iter(lista) |
next(it) | Pide el siguiente valor | next(it, "fin") |
StopIteration | Aviso de que no quedan elementos | La captura for por ti |
__iter__ / __next__ | Métodos para crear un iterador propio | return self / raise StopIteration |
yield | Entrega un valor y pausa la función | yield km |
| Expresión generadora | Comprensión perezosa con paréntesis | sum(p for p in precios) |
yield from | Produce todos los valores de otro iterable | yield from tareas |
count, islice, chain | Iteradores de itertools | islice(gen, 3) |
En la siguiente lección usarás funciones que envuelven a otras funciones: los decoradores.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.