Iteradores y generadores en Python — ejercicios para dominar yield e iter
Los iteradores y generadores en Python ejercicios con solución cierran este bloque. Ya viste la teoría y practicaste con programas reales. Ahora toca resolver por tu cuenta. Tres ejercicios en tres niveles con especial atención a los dos errores que más aparecen en FP2: confundir iterable con iterador al implementar __iter__, y olvidar que un generador agotado no se puede reiniciar.
Como siempre: intenta resolverlo, usa la pista si llevas más de 10 minutos atascado, revisa posibles errores en pythontutor.com y compara con la solución comentada al final.
Tabla de Contenidos
Iteradores y generadores en Python ejercicios — Nivel Básico
Ejercicio 1 — Generador de múltiplos de un número
Escribe dos versiones de un generador de múltiplos: una finita y una infinita. La versión finita recibe la base y un límite. La versión infinita solo recibe la base y genera múltiplos sin parar.
La salida debe ser:
Múltiplos de 3 hasta 30: 3 6 9 12 15 18 21 24 27 30 Primeros 8 múltiplos de 7: 7 14 21 28 35 42 49 56 Múltiplos de 5 entre 20 y 50: 20 25 30 35 40 45 50 Múltiplos comunes de 3 y 5 hasta 100: 15 30 45 60 75 90
💡 Pista — el error del generador agotado:
# MAL — intentar reutilizar un generador agotado gen = multiplos_hasta(3, 30) lista1 = list(gen) # consume todo el generador lista2 = list(gen) # → [] vacío — el generador ya se agotó # BIEN — crear un nuevo generador cada vez lista1 = list(multiplos_hasta(3, 30)) lista2 = list(multiplos_hasta(3, 30)) # nuevo generador, funciona
Un generador es como un libro que se va destruyendo página a página según lo lees, cuando terminas, ya no queda nada. Si quieres leerlo otra vez, necesitas un ejemplar nuevo. Si necesitas recorrer la secuencia más de una vez, o bien creas el generador de nuevo cada vez, o conviertes el resultado en una lista desde el principio.
Para los múltiplos comunes de 3 y 5, combina los dos generadores infinitos con una condición. No hace falta nada más elaborado.
Iteradores y generadores en Python ejercicios — Nivel Intermedio
Ejercicio 2 — Iterador de secuencia aritmética
Implementa una clase SecuenciaAritmetica que sea un iterador completo. Una secuencia aritmética tiene un valor inicial, un paso constante y un número máximo de términos. Por ejemplo, SecuenciaAritmetica(2, 3, 5) genera 2, 5, 8, 11, 14, empieza en 2, suma 3 en cada paso, genera 5 términos.
Requisitos:
- Implementa
__iter__y__next__ - Valida en
__init__que el paso no sea 0 y que los términos sean positivos — lanzaValueErrorsi no - Implementa
__len__que devuelve el número de términos que quedan - Implementa
__str__que muestra la secuencia completa - La secuencia debe poder recorrerse con
for, conlist(), consum()y connext()directamente
La salida debe ser:
Secuencia(2, paso=3, términos=5): for: 2 5 8 11 14 list: [2, 5, 8, 11, 14] sum: 40 str: 2 → 5 → 8 → 11 → 14 Secuencia(10, paso=-2, términos=6): for: 10 8 6 4 2 0 Secuencia(0, paso=0.5, términos=8): for: 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 Error esperado: El paso no puede ser 0
💡 Pista — el error de confundir iterable con iterador:
# MAL — __iter__ no reinicia el estado
class SecuenciaAritmetica:
def __init__(self, inicio, paso, terminos):
self.actual = inicio # el estado está en __init__
self.paso = paso
self.restantes = terminos
def __iter__(self):
return self # se devuelve a sí mismo SIN reiniciar
def __next__(self):
if self.restantes == 0:
raise StopIteration
valor = self.actual
self.actual += self.paso
self.restantes -= 1
return valor
seq = SecuenciaAritmetica(2, 3, 5)
print(list(seq)) # → [2, 5, 8, 11, 14] — bien
print(list(seq)) # → [] — MAL, el iterador se agotó en el primer list()
# BIEN — __iter__ reinicia el estado cada vez que se llama
def __iter__(self):
self.__actual = self.__inicio # reinicia al valor original
self.__restantes = self.__terminos # reinicia el contador
return self
Cuando haces list(seq) Python llama a iter(seq) que llama a __iter__. Si __iter__ no reinicia el estado, la segunda llamada a list(seq) encuentra el iterador ya agotado y devuelve una lista vacía. La solución es guardar los valores originales en __init__ y restaurarlos en __iter__.
Iteradores y generadores en Python ejercicios — Nivel Desafío
Ejercicio 3 — Generador de números perfectos
Un número perfecto es aquel cuya suma de divisores propios (todos los divisores excepto él mismo) es igual al número. El 6 es perfecto porque 1 + 2 + 3 = 6. El 28 también: 1 + 2 + 4 + 7 + 14 = 28.
Escribe:
- Una función
es_perfecto(n)que comprueba si un número es perfecto - Un generador infinito
perfectos()que genera números perfectos uno a uno - Un generador
perfectos_hasta(limite)que genera perfectos hasta un límite
Hay muy pocos números perfectos, los cuatro primeros son 6, 28, 496 y 8128. El siguiente es 33550336. Para el ejercicio, genera los tres primeros que encontrarás por debajo de 10000.
La salida debe ser:
¿Es 6 perfecto? True ¿Es 12 perfecto? False ¿Es 28 perfecto? True Perfectos hasta 10000: 6 28 496 8128 Divisores de 6: [1, 2, 3] → suma: 6 Divisores de 28: [1, 2, 4, 7, 14] → suma: 28 Divisores de 496: [1, 2, 4, 8, 16, 31, 62, 124, 248] → suma: 496
💡 Pistas:
- Para encontrar los divisores propios de
nitera desde 1 hastan-1y comprueba sin % i == 0, pero puedes optimizarlo iterando solo hastan//2 - El generador
perfectos()tiene que ser infinito, empieza en 2 y va subiendo sin parar - El generador
perfectos_hasta(limite)puede usarreturnpara señalar el fin, igual que hicimos conprimos_hastaen el artículo de práctica. - El error del generador agotado aparece si intentas llamar a
list(perfectos_hasta(10000))dos veces sobre el mismo objeto generador — la segunda da vacío
Soluciones Comentadas
Solución Ejercicio 1:
def multiplos_infinitos(base):
"""Generador infinito de múltiplos de base."""
multiplo = base
while True:
yield multiplo
multiplo += base
def multiplos_hasta(base, limite):
"""Generador de múltiplos de base hasta limite."""
multiplo = base
while multiplo <= limite:
yield multiplo
multiplo += base
def primeros_n_multiplos(base, n):
"""Generador de los primeros n múltiplos de base."""
contador = 0
for m in multiplos_infinitos(base):
if contador >= n:
return
yield m
contador += 1
# Múltiplos de 3 hasta 30
print("Múltiplos de 3 hasta 30:")
for m in multiplos_hasta(3, 30):
print(m, end=' ')
print()
# Primeros 8 múltiplos de 7
print("\nPrimeros 8 múltiplos de 7:")
for m in primeros_n_multiplos(7, 8):
print(m, end=' ')
print()
# Múltiplos de 5 entre 20 y 50
print("\nMúltiplos de 5 entre 20 y 50:")
for m in multiplos_hasta(5, 50):
if m >= 20:
print(m, end=' ')
print()
# Múltiplos comunes de 3 y 5 hasta 100
print("\nMúltiplos comunes de 3 y 5 hasta 100:")
for m in multiplos_hasta(3, 100):
if m % 5 == 0:
print(m, end=' ')
print()
Solución Ejercicio 2:
class SecuenciaAritmetica:
def __init__(self, inicio, paso, terminos):
if paso == 0:
raise ValueError('El paso no puede ser 0')
if not isinstance(terminos, int) or terminos <= 0:
raise ValueError('Los términos deben ser un entero positivo')
self.__inicio = inicio
self.__paso = paso
self.__terminos = terminos
# estado del iterador — se reinicia en __iter__
self.__actual = inicio
self.__restantes = terminos
def __iter__(self):
self.__actual = self.__inicio # reinicia al principio
self.__restantes = self.__terminos # reinicia el contador
return self
def __next__(self):
if self.__restantes == 0:
raise StopIteration
valor = self.__actual
self.__actual += self.__paso
self.__restantes -= 1
return valor
def __len__(self):
return self.__restantes
def __str__(self):
terminos = []
actual = self.__inicio
for _ in range(self.__terminos):
terminos.append(str(actual))
actual += self.__paso
return ' → '.join(terminos)
def __repr__(self):
return (f'SecuenciaAritmetica({self.__inicio}, '
f'paso={self.__paso}, términos={self.__terminos})')
# Prueba completa
seq1 = SecuenciaAritmetica(2, 3, 5)
print(f"Secuencia(2, paso=3, términos=5):")
print(f" for: ", end='')
for v in seq1:
print(v, end=' ')
print()
print(f" list: {list(seq1)}") # funciona porque __iter__ reinicia
print(f" sum: {sum(seq1)}")
print(f" str: {seq1}")
seq2 = SecuenciaAritmetica(10, -2, 6)
print(f"\nSecuencia(10, paso=-2, términos=6):")
print(f" for: ", end='')
for v in seq2:
print(v, end=' ')
print()
seq3 = SecuenciaAritmetica(0, 0.5, 8)
print(f"\nSecuencia(0, paso=0.5, términos=8):")
print(f" for: ", end='')
for v in seq3:
print(v, end=' ')
print()
try:
seq_mala = SecuenciaAritmetica(1, 0, 5)
except ValueError as err:
print(f"\nError esperado: {err}")
Solución Ejercicio 3:
def divisores_propios(n):
"""Devuelve lista de divisores propios de n."""
if n < 2:
return []
divisores = [1]
for i in range(2, n // 2 + 1):
if n % i == 0:
divisores.append(i)
return divisores
def es_perfecto(n):
"""Comprueba si n es un número perfecto."""
if n < 2:
return False
return sum(divisores_propios(n)) == n
def perfectos():
"""Generador infinito de números perfectos."""
candidato = 2
while True:
if es_perfecto(candidato):
yield candidato
candidato += 1
def perfectos_hasta(limite):
"""Generador de números perfectos hasta limite."""
for p in perfectos():
if p > limite:
return # StopIteration — señal de fin
yield p
# Comprobaciones individuales
print(f"¿Es 6 perfecto? {es_perfecto(6)}")
print(f"¿Es 12 perfecto? {es_perfecto(12)}")
print(f"¿Es 28 perfecto? {es_perfecto(28)}")
# Perfectos hasta 10000
print("\nPerfectos hasta 10000:")
for p in perfectos_hasta(10000):
print(p, end=' ')
print()
# Divisores de cada perfecto hasta 10000
print()
for p in perfectos_hasta(10000):
divs = divisores_propios(p)
print(f"Divisores de {p}: {divs} → suma: {sum(divs)}")
Chuletario — Iteradores y generadores en Python
# ============================================
# CHULETARIO — Iteradores y generadores
# Sergio Learns · sergiolearns.com
# ============================================
# ITERABLE vs ITERADOR
# Iterable → tiene __iter__() → puedes usar for con él
# Iterador → tiene __iter__() y __next__() → produce elementos uno a uno
# Un iterador es siempre iterable pero no al revés
# FUNCIONES BUILT-IN
it = iter(iterable) # convierte iterable en iterador
valor = next(it) # devuelve el siguiente elemento
# cuando se agota → StopIteration
# LO QUE HACE EL FOR POR DENTRO
for x in obj:
cuerpo
# equivale a:
it = iter(obj)
while True:
try:
x = next(it)
cuerpo
except StopIteration:
break
# ITERADOR PROPIO — con __iter__ y __next__
class MiIterador:
def __init__(self, inicio, fin, paso=1):
self.__inicio = inicio
self.__fin = fin
self.__paso = paso
def __iter__(self):
self.__actual = self.__inicio # REINICIAR aquí — no en __init__
return self # devuelve self
def __next__(self):
if self.__actual > self.__fin:
raise StopIteration # señal de fin
valor = self.__actual
self.__actual += self.__paso
return valor
# GENERADOR — la forma pythónica
def mi_generador(inicio, fin, paso=1):
actual = inicio
while actual <= fin:
yield actual # pausa + devuelve + continúa después
actual += paso
# GENERADOR INFINITO
def infinito():
n = 0
while True:
yield n
n += 1
# RETURN EN UN GENERADOR → StopIteration
def hasta_limite(limite):
for x in infinito():
if x > limite:
return # lanza StopIteration, no termina con valor
yield x
# YIELD vs RETURN
# return → termina la función, el estado desaparece
# yield → pausa la función, el estado persiste, continúa con next()
# __iter__ COMO GENERADOR EN UNA CLASE
class MiContenedor:
def __iter__(self):
nodo = self.__primero
while nodo is not None:
yield nodo.valor # generador dentro de __iter__
nodo = nodo.siguiente
# ventaja: cada llamada a iter() crea un generador nuevo → múltiples recorridos
# GENERADOR vs LISTA
# Lista → todos los elementos en memoria desde el principio
# Generador → un elemento en memoria en cada momento
lista = [i * 2 for i in range(1_000_000)] # ~8MB
gen = (i * 2 for i in range(1_000_000)) # unos bytes — expresión generadora
# ENCADENAR GENERADORES
def pares():
n = 0
while True:
yield n
n += 2
def pares_hasta(limite):
for p in pares(): # consume pares() elemento a elemento
if p > limite:
return
yield p
# ERRORES TÍPICOS
# 1. GENERADOR AGOTADO — no se puede reiniciar
gen = mi_generador(1, 5)
list(gen) # → [1, 2, 3, 4, 5] — consume el generador
list(gen) # → [] — agotado, no hay más
# SOLUCIÓN: crear un nuevo generador con mi_generador(1, 5) de nuevo
# 2. __iter__ que no reinicia el estado
class Mal:
def __iter__(self):
return self # sin reiniciar → segunda vez da vacío
class Bien:
def __iter__(self):
self.__actual = self.__inicio # reinicia
return self
# 3. for sobre generador infinito sin break → bucle infinito
for n in infinito():
print(n) # nunca para — siempre añade break o condición
# 4. Confundir yield con return en generadores
def mal():
return 1 # función normal — devuelve 1 y termina
return 2 # nunca se ejecuta
def bien():
yield 1 # pausa y devuelve 1
yield 2 # pausa y devuelve 2 en el siguiente next()
# 5. next() sobre un iterador agotado → StopIteration sin capturar
it = iter([1])
next(it) # → 1
next(it) # → StopIteration — captura si no estás en un for
# PATRÓN COMPLETO — iterador de clase con reinicio
class SecuenciaCompleta:
def __init__(self, inicio, fin):
self.__inicio = inicio
self.__fin = fin
def __iter__(self):
self.__actual = self.__inicio # reinicia cada vez
return self
def __next__(self):
if self.__actual > self.__fin:
raise StopIteration
valor = self.__actual
self.__actual += 1
return valor
def __len__(self):
return max(0, self.__fin - self.__inicio + 1)
Sigue aprendiendoTodos los artículos de Python¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

Un comentario