Iteradores y generadores en Python — cómo funciona el for por dentro
Los iteradores y generadores en Python son de esos temas de FP2 que al principio parecen un concepto abstracto y difícil de visualizar, y que de repente hacen clic cuando entiendes que llevas usándolos desde el primer día sin saberlo. Cada vez que escribes un for en Python estás usando un iterador.
En este artículo vemos qué hay detrás de eso, cómo construir los tuyos propios y por qué los generadores son la forma más elegante de hacerlo.
Tabla de Contenidos
Antes de empezar — qué diferencia un iterable de un iterador
Esta es la confusión más común en FP2 y la que más vale aclarar desde el principio, porque los dos términos suenan muy parecidos pero no son lo mismo.
Iterable — cualquier objeto que puedes recorrer con un for. Una lista, una cadena, un diccionario, un rango, todos son iterables. Lo que los hace iterables es que tienen un método __iter__() que devuelve un iterador.
Iterador — el objeto que realmente hace el recorrido, elemento a elemento. Tiene dos métodos: __iter__() que se devuelve a sí mismo, y __next__() que devuelve el siguiente elemento. Cuando no quedan más elementos lanza StopIteration.
La analogía que más me ayudó a entenderlo: un libro es el iterable: puedes leerlo. El dedo que va señalando la página actual es el iterador: sabe en qué punto estás y avanza uno a uno.
# Una lista es iterable pero no es un iterador mi_lista = [1, 2, 3] print(type(mi_lista)) # → <class 'list'> # iter() convierte el iterable en iterador mi_iterador = iter(mi_lista) print(type(mi_iterador)) # → <class 'list_iterator'>
Las funciones iter() y next() — el bucle for por dentro
Cuando escribes un for en Python, Python no hace magia, ejecuta exactamente esto por debajo:
# Lo que TÚ escribes
for numero in [1, 2, 3]:
print(numero)
# Lo que Python REALMENTE hace
lista = [1, 2, 3]
iterador = iter(lista) # llama a lista.__iter__()
while True:
try:
numero = next(iterador) # llama a iterador.__next__()
print(numero)
except StopIteration:
break # el iterador se agotó → sale del bucle
iter() llama al método __iter__() del objeto. next() llama al método __next__(). Cuando no hay más elementos __next__() lanza StopIteration y el while termina. Eso es todo lo que hay detrás de cada for que hayas escrito en FP1.
Puedes usar iter() y next() directamente:
numeros = [10, 20, 30] it = iter(numeros) print(next(it)) # → 10 print(next(it)) # → 20 print(next(it)) # → 30 print(next(it)) # → StopIteration — no hay más elementos
Fíjate en algo importante: una vez que el iterador se agota no vuelve al principio. Si quieres recorrer la lista otra vez necesitas crear un nuevo iterador con iter() otra vez.
Construir tu propio iterador — los métodos iter y next
Para que una clase sea un iterador tiene que implementar dos métodos mágicos: __iter__() y __next__(). Vamos a construir uno que genere una secuencia de números dentro de un rango, igual que el range() de Python pero hecho por nosotros:
class MiRango:
def __init__(self, inicio=0, fin=0, paso=1):
self.inicio = inicio
self.fin = fin
self.paso = paso
def __iter__(self):
self.actual = self.inicio # reinicia al principio
return self # se devuelve a sí mismo
def __next__(self):
if self.actual <= self.fin:
resultado = self.actual
self.actual += self.paso
return resultado
else:
raise StopIteration # señal de que se acabó
Ahora puedes usarlo exactamente igual que range():
for i in MiRango(1, 5):
print(i)
# → 1, 2, 3, 4, 5
for i in MiRango(0, 10, 2):
print(i)
# → 0, 2, 4, 6, 8, 10
# También puedes usar iter() y next() directamente
mi_rango = MiRango(5, 8)
it = iter(mi_rango)
print(next(it)) # → 5
print(next(it)) # → 6
La ventaja importante de los iteradores es que no generan todos los elementos a la vez en memoria, los producen uno a uno cuando se necesitan. Si tienes un millón de números, un iterador usa solo la memoria de un número en cada momento. Una lista de un millón de números ocupa toda esa memoria desde el principio.
El problema del iterador propio — iterable vs iterador
Fíjate en algo que puede dar problemas en FP2. La clase MiRango de arriba es iterador e iterable a la vez, __iter__ se devuelve a sí mismo. Eso significa que si intentas usarla en dos bucles anidados a la vez, el segundo bucle interfiere con el primero porque comparten el mismo estado (self.actual):
rango = MiRango(1, 3)
for i in rango:
for j in rango: # problema — comparten el mismo iterador
print(i, j)
La solución que verás en el PDF de FP2 es separar el iterable del iterador, el iterable crea un iterador nuevo cada vez que se llama a __iter__(). Pero para los ejercicios de FP2 la versión simple (iterador que se devuelve a sí mismo) es suficiente.
Generadores — la forma pythónica de hacer iteradores
Construir un iterador con __iter__ y __next__ funciona pero es verboso. Los generadores son la respuesta de Python a eso, parecen funciones normales pero usan yield en vez de return:
def mi_rango_generador(inicio, fin, paso=1):
actual = inicio
while actual <= fin:
yield actual # pausa aquí y devuelve el valor
actual += paso # continúa desde aquí la próxima vez
Se usa exactamente igual que el iterador anterior:
for i in mi_rango_generador(1, 5):
print(i)
# → 1, 2, 3, 4, 5
Por qué yield pausa la función en vez de terminarla
Esta es la parte que más cuesta visualizar en FP2, así que vamos despacio.
Cuando Python ejecuta return, la función termina, su estado desaparece y el valor se devuelve al que llamó. Cuando Python ejecuta yield, la función se pausa, guarda todo su estado (variables locales, punto de ejecución) y devuelve el valor. La próxima vez que se llama a next() la función continúa exactamente desde donde se quedó.
def mi_generador():
print("Antes del primero")
yield 1
print("Antes del segundo")
yield 2
print("Antes del tercero")
yield 3
print("Se acabó")
gen = mi_generador()
print(next(gen)) # imprime "Antes del primero" → devuelve 1
print(next(gen)) # imprime "Antes del segundo" → devuelve 2
print(next(gen)) # imprime "Antes del tercero" → devuelve 3
print(next(gen)) # imprime "Se acabó" → StopIteration
Antes del primero 1 Antes del segundo 2 Antes del tercero 3 Se acabó StopIteration
Fíjate en que print("Antes del primero") no se ejecuta cuando llamas a mi_generador(), solo cuando llamas a next() por primera vez. Llamar a la función no la ejecuta, solo crea el objeto generador. La ejecución empieza con el primer next().
La analogía que mejor funciona: imagina que el generador es un libro de recetas y yield es un marcapáginas. Cada vez que llegas a un yield pones el marcapáginas ahí y cierras el libro. La próxima vez que lo abres, continúas exactamente desde el marcapáginas, no desde el principio.
Generadores con bucles — el uso más común
El patrón más habitual en FP2 es un generador con un while o un for interno:
def rango_generador(inicio, fin, paso=1):
actual = inicio
while actual <= fin:
yield actual
actual += paso
# Uso en bucle for
for numero in rango_generador(3, 20, 3):
print(numero)
# → 3, 6, 9, 12, 15, 18
# Uso con iter() y next() explícitos
gen = rango_generador(3, 20, 3)
while True:
try:
i = next(gen)
print(i)
except StopIteration:
break
Los dos bloques producen exactamente la misma salida, el for usa iter() y next() por debajo automáticamente.
Generador vs lista — cuándo usar cada uno
Esta es la pregunta que más aparece en FP2 y en la que más vale tener la respuesta clara.
Usa una lista cuando:
- Necesitas todos los elementos a la vez
- Vas a recorrer la secuencia más de una vez
- Necesitas acceder a elementos por índice (
lista[3]) - La secuencia es pequeña
Usa un generador cuando:
- Los elementos se usan uno a uno y no se necesitan todos a la vez
- La secuencia es muy larga o infinita
- Quieres ahorrar memoria
- Los elementos son costosos de calcular y solo necesitas algunos
# Lista — genera todos a la vez en memoria
lista_millones = [i * 2 for i in range(1_000_000)]
# ocupa ~8MB en memoria desde el principio
# Generador — genera uno a uno
def millones_generador():
for i in range(1_000_000):
yield i * 2
# ocupa solo unos bytes — da igual lo grande que sea
gen = millones_generador()
La regla práctica: si solo vas a recorrer la secuencia una vez de principio a fin, un generador es mejor. Si necesitas más flexibilidad, usa una lista.
Un generador infinito — algo imposible con una lista
Los generadores pueden ser infinito, algo que con una lista es imposible:
def numeros_naturales():
n = 1
while True: # bucle infinito — el generador nunca lanza StopIteration
yield n
n += 1
gen = numeros_naturales()
print(next(gen)) # → 1
print(next(gen)) # → 2
print(next(gen)) # → 3
# puedes llamar a next() tantas veces como quieras
Úsalos con cuidado en bucles for, un for sobre un generador infinito nunca termina:
for n in numeros_naturales():
if n > 10:
break
print(n)
# → 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
Visualízalo con Python Tutor
Los generadores son donde Python Tutor más brilla, puedes ver exactamente cómo la función se pausa y retoma. Ve a pythontutor.com y pega este código:
def mi_generador():
yield 1
yield 2
yield 3
gen = mi_generador()
a = next(gen)
b = next(gen)
c = next(gen)
print(a, b, c)
Ejecuta paso a paso y observa tres cosas: cuando llamas a mi_generador() la función no se ejecuta, solo se crea el objeto generador. Cuando llamas a next(gen) la función avanza hasta el yield, pausa y devuelve el valor. El estado de la función (el punto de ejecución) persiste entre llamadas a next(). Cada yield es como un punto de parada.
Luego añade print() entre los yield como hicimos antes y vuelve a ejecutar paso a paso, así verás el orden exacto de ejecución.
Resumen rápido
# ITERABLE vs ITERADOR
# Iterable → tiene __iter__() → puedes recorrerlo con for
# Iterador → tiene __iter__() y __next__() → produce elementos uno a uno
# iter() y next()
it = iter([1, 2, 3]) # crea iterador desde iterable
next(it) # devuelve 1, luego 2, luego 3
# cuando se acaba → StopIteration
# Lo que hace el for por dentro
for x in iterable:
...
# equivale a:
it = iter(iterable)
while True:
try: x = next(it)
except StopIteration: break
# ITERADOR PROPIO
class MiIterador:
def __iter__(self):
self.actual = self.inicio
return self # se devuelve a sí mismo
def __next__(self):
if condicion:
resultado = self.actual
self.actual += paso
return resultado
else:
raise StopIteration # señal de fin
# GENERADOR — la forma pythónica
def mi_generador(inicio, fin):
actual = inicio
while actual <= fin:
yield actual # pausa + devuelve valor
actual += 1 # continúa desde aquí la próxima vez
# yield vs return
# return → termina la función, el estado desaparece
# yield → pausa la función, el estado persiste, continúa con next()
# CUÁNDO USAR CADA UNO
# Lista → varios recorridos, acceso por índice, secuencia pequeña
# Generador → un recorrido, secuencia grande o infinita, ahorro de memoria
# ERRORES TÍPICOS
# 1. Confundir iterable con iterador → una lista es iterable, no iterador
# 2. Intentar reutilizar un iterador agotado → crear uno nuevo con iter()
# 3. for sobre generador infinito sin break → bucle infinito
# 4. Pensar que yield es como return → yield pausa, no termina
En el próximo artículo practicamos iteradores y generadores con programas reales, incluyendo un generador de números de Fibonacci infinito y un iterador para una lista encadenada.
Siguiente paso: prácticaIteradores y generadores en Python — 3 programas reales que los hacen tangibles¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

2 comentarios