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.


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 — lanza ValueError si 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, con list(), con sum() y con next() 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 n itera desde 1 hasta n-1 y comprueba si n % i == 0, pero puedes optimizarlo iterando solo hasta n//2
  • El generador perfectos() tiene que ser infinito, empieza en 2 y va subiendo sin parar
  • El generador perfectos_hasta(limite) puede usar return para señalar el fin, igual que hicimos con primos_hasta en 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 →

Sigue aprendiendo

Un comentario

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *