Iteradores y generadores en Python — 3 programas reales que los hacen tangibles
Los iteradores y generadores en Python práctica real es lo que toca ahora. En el artículo anterior vimos la teoría, qué es un iterable, cómo funciona iter() y next() por dentro, y por qué yield pausa en vez de terminar. Ahora escribimos tres programas reales donde los iteradores y generadores no son un ejercicio académico sino la herramienta más natural para el problema.
Tabla de Contenidos
Iteradores y generadores en Python práctica — Programa 1: Generador de Fibonacci infinito
La sucesión de Fibonacci es el ejemplo perfecto para un generador infinito, cada número depende del anterior, la secuencia no tiene fin, y en la práctica solo necesitamos los primeros N números o los que cumplan alguna condición. Con una lista tendríamos que decidir de antemano cuántos generar. Con un generador simplemente pedimos los que necesitemos.
Recuerda la sucesión: 0, 1, 1, 2, 3, 5, 8, 13, 21… — cada número es la suma de los dos anteriores.
def fibonacci():
"""Generador infinito de la sucesión de Fibonacci."""
anterior = 0
actual = 1
while True:
yield anterior # devuelve el valor actual y pausa
anterior, actual = actual, anterior + actual # avanza
# Los primeros 10 números de Fibonacci
print("Primeros 10 números de Fibonacci:")
gen = fibonacci()
for i in range(10):
print(next(gen), end=' ')
print()
# → 0 1 1 2 3 5 8 13 21 34
# Fibonacci hasta un límite
print("\nFibonacci hasta 100:")
for n in fibonacci():
if n > 100:
break
print(n, end=' ')
print()
# → 0 1 1 2 3 5 8 13 21 34 55 89
# El décimo número de Fibonacci
gen = fibonacci()
for _ in range(9):
next(gen)
print(f"\nDécimo número de Fibonacci: {next(gen)}")
# → 34
# Fibonacci pares hasta 500
print("\nFibonacci pares hasta 500:")
for n in fibonacci():
if n > 500:
break
if n % 2 == 0:
print(n, end=' ')
print()
# → 0 2 8 34 144 376 — no todos, solo los pares
Fíjate en la línea clave del generador:
anterior, actual = actual, anterior + actual
Python evalúa toda la parte derecha antes de hacer las asignaciones, así no necesitas una variable temporal. Es la forma pythónica de intercambiar y actualizar dos valores a la vez.
Lo más importante: el generador no sabe cuántos números vas a necesitar y tampoco le importa. Tú decides desde fuera cuándo parar, con range(), con break, con una condición. El generador solo hace su trabajo: dar el siguiente número cuando se lo pides.
Iteradores y generadores en Python práctica — Programa 2: Generador de números primos
Los números primos son otro caso ideal para un generador, la secuencia es infinita y el coste de calcular si un número es primo no es despreciable, así que tiene sentido producirlos solo cuando se necesiten.
def es_primo(n):
"""Comprueba si n es primo. Función auxiliar."""
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
# solo comprueba divisores hasta la raíz cuadrada — optimización clave
i = 3
while i * i <= n:
if n % i == 0:
return False
i += 2 # solo impares
return True
def primos():
"""Generador infinito de números primos."""
yield 2 # el único par primo — caso especial
candidato = 3
while True:
if es_primo(candidato):
yield candidato
candidato += 2 # solo comprobamos impares
def primos_hasta(limite):
"""Generador de primos hasta un límite dado."""
for p in primos():
if p > limite:
return # return en un generador → StopIteration
yield p
def n_primos(n):
"""Generador de los primeros n primos."""
contador = 0
for p in primos():
if contador >= n:
return
yield p
contador += 1
# Los primeros 15 primos
print("Primeros 15 primos:")
for p in n_primos(15):
print(p, end=' ')
print()
# → 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
# Primos hasta 50
print("\nPrimos hasta 50:")
for p in primos_hasta(50):
print(p, end=' ')
print()
# → 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
# ¿Cuántos primos hay hasta 100?
total = sum(1 for _ in primos_hasta(100))
print(f"\nPrimos hasta 100: {total}")
# → 25
# El primo número 50
gen = primos()
for _ in range(49):
next(gen)
print(f"El primo número 50: {next(gen)}")
# → 229
# Primos entre 100 y 150
print("\nPrimos entre 100 y 150:")
for p in primos():
if p > 150:
break
if p >= 100:
print(p, end=' ')
print()
# → 101 103 107 109 113 127 131 137 139 149
Hay un detalle importante en primos_hasta y n_primos, usan return dentro de un generador. Esto no termina la función como en una función normal, lanza StopIteration, que es exactamente la señal que necesita el for para saber que se acabó. Es la forma correcta de terminar un generador antes de que se acabe el while True.
Fíjate también en cómo n_primos y primos_hasta son generadores que usan el generador primos() por dentro — puedes encadenar generadores. Cada uno consume al anterior elemento a elemento, sin cargar nada extra en memoria.
Iteradores y generadores en Python práctica — Programa 3: Iterador para lista encadenada (LinkedList)
Nota importante: Este programa usa una
LinkedList: una estructura de datos encadenada que es el último bloque del temario de FP2. Si todavía no has llegado a ese tema en clase, guarda este programa para cuando lo veas, todo lo que necesitas saber sobre iteradores ya está en los dos programas anteriores. Si ya lo has visto o tienes curiosidad, sigue leyendo.
En el bloque de estructuras encadenadas de FP2 construirás tu propia LinkedList — una lista donde cada elemento (nodo) guarda el valor y una referencia al siguiente nodo. La forma natural de recorrerla es con un iterador.
class LinkedList:
"""Lista encadenada simple con iterador integrado."""
class Nodo:
"""Nodo interno de la lista."""
def __init__(self, valor, siguiente=None):
self.valor = valor
self.siguiente = siguiente
def __init__(self):
self.__primero = None
self.__longitud = 0
def insertar_delante(self, valor):
"""Inserta un elemento al principio."""
self.__primero = self.Nodo(valor, self.__primero)
self.__longitud += 1
def insertar_detras(self, valor):
"""Inserta un elemento al final."""
nuevo = self.Nodo(valor)
if self.__primero is None:
self.__primero = nuevo
else:
actual = self.__primero
while actual.siguiente is not None:
actual = actual.siguiente
actual.siguiente = nuevo
self.__longitud += 1
def __len__(self):
return self.__longitud
def __iter__(self):
"""Hace la LinkedList iterable usando un generador."""
actual = self.__primero
while actual is not None:
valor = actual.valor
actual = actual.siguiente
yield valor # pausa aquí y devuelve el valor
def __str__(self):
return ' → '.join(str(v) for v in self) + ' → None'
def __repr__(self):
return f'LinkedList({list(self)})'
Lo más interesante de este programa está en __iter__, es un generador dentro de un método mágico. Python acepta perfectamente que __iter__ sea una función generadora. Cuando llamas a iter(mi_lista) o usas la lista en un for, Python llama a __iter__() que devuelve un objeto generador, y ese generador va produciendo los valores nodo a nodo.
# Construir la lista
lista = LinkedList()
lista.insertar_detras(10)
lista.insertar_detras(20)
lista.insertar_detras(30)
lista.insertar_detras(40)
lista.insertar_detras(50)
print(f"Lista: {lista}")
# → 10 → 20 → 30 → 40 → 50 → None
print(f"Longitud: {len(lista)}")
# → 5
# Recorrer con for — usa __iter__ automáticamente
print("\nRecorrido con for:")
for valor in lista:
print(valor, end=' ')
print()
# → 10 20 30 40 50
# Recorrer con iter() y next() explícito
print("\nRecorrido con iter() y next():")
it = iter(lista)
print(next(it)) # → 10
print(next(it)) # → 20
print(next(it)) # → 30
# Usar en comprensiones y funciones built-in
print(f"\nSuma: {sum(lista)}")
print(f"Máximo: {max(lista)}")
print(f"Mínimo: {min(lista)}")
print(f"Como lista: {list(lista)}")
# → Suma: 150
# → Máximo: 50
# → Mínimo: 10
# → Como lista: [10, 20, 30, 40, 50]
# Varios recorridos independientes — cada for crea un iterador nuevo
print("\nVarios recorridos independientes:")
for v in lista:
print(f" Externo: {v}")
# un segundo for sobre la misma lista funciona correctamente
# porque __iter__ crea un nuevo generador cada vez
# Recorrido múltiple — funciona porque __iter__ se llama de nuevo
print("\nLista recorrida dos veces:")
print("Primera vez:", list(lista))
print("Segunda vez:", list(lista))
# → ambas dan [10, 20, 30, 40, 50] — no se agota
Fíjate en la diferencia clave respecto al iterador simple del artículo de teoría: aquí __iter__ es un generador que crea un objeto nuevo cada vez que se llama. Eso significa que puedes recorrer la lista tantas veces como quieras y funciona correctamente, a diferencia del iterador que se devuelve a sí mismo, que se agotaría tras el primer recorrido.
El patrón que debes llevarte
De los tres programas hay un patrón que se repite y que vale la pena interiorizar:
# PATRÓN 1 — generador infinito con while True
def secuencia_infinita():
estado_inicial = ...
while True:
yield valor_calculado
actualizar_estado()
# PATRÓN 2 — generador finito con return como señal de fin
def secuencia_finita(limite):
for elemento in secuencia_infinita():
if condicion_de_fin:
return # lanza StopIteration
yield elemento
# PATRÓN 3 — __iter__ como generador en una clase
class MiContenedor:
def __iter__(self):
nodo = self.__primero
while nodo is not None:
yield nodo.valor
nodo = nodo.siguiente
Visualízalo con Python Tutor
Copia este generador simplificado en pythontutor.com:
def fibonacci():
anterior = 0
actual = 1
while True:
yield anterior
anterior, actual = actual, anterior + actual
gen = fibonacci()
a = next(gen) # 0
b = next(gen) # 1
c = next(gen) # 1
d = next(gen) # 2
e = next(gen) # 3
print(a, b, c, d, e)
Ejecuta paso a paso y observa cómo cada next() retoma la función exactamente donde la dejó el yield anterior. Las variables anterior y actual conservan sus valores entre llamadas, ahí está la magia del generador.
Resumen y siguiente paso
En este artículo practicaste iteradores y generadores con tres programas reales. El generador de Fibonacci muestra el patrón infinito más limpio que existe. El generador de primos demuestra cómo encadenar generadores y usar return como señal de fin. La LinkedList muestra que __iter__ puede ser un generador, y por qué eso lo hace más flexible que devolver self.
En el siguiente artículo encontrarás ejercicios propuestos con solución para practicar por tu cuenta.

2 comentarios