iteradores y generadores en Python ejercicios resueltos chuletario

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)

Python iterators and generators — exercises to master yield and iter

Iterator and generator exercises close out this block. You’ve seen the theory and practised with real programs. Now solve on your own. Three exercises across three levels, focusing on the two most common FP2 mistakes: confusing iterable with iterator when implementing __iter__, and forgetting that an exhausted generator can’t be restarted.

As always: try to solve it yourself, use the hint if you’ve been stuck for more than 10 minutes, and compare with the commented solution at the end.

Basic Level

Exercise 1 — Multiples generator

Write two versions: a finite one that receives the base and a limit, and an infinite one that only receives the base. Produce: multiples of 3 up to 30, first 8 multiples of 7, multiples of 5 between 20 and 50, common multiples of 3 and 5 up to 100.

💡 Hint — exhausted generator mistake:

# WRONG — reusing an exhausted generator
gen = multiples_up_to(3, 30)
list1 = list(gen)    # consumes the generator
list2 = list(gen)    # → [] empty — already exhausted

# RIGHT — create a new generator each time
list1 = list(multiples_up_to(3, 30))
list2 = list(multiples_up_to(3, 30))    # new generator, works

A generator is like a book that gets destroyed page by page as you read it. Once you finish, nothing remains. For common multiples of 3 and 5 — combine both generators with a condition.

Intermediate Level

Exercise 2 — Arithmetic sequence iterator

Implement ArithmeticSequence as a complete iterator class. ArithmeticSequence(2, 3, 5) generates 2, 5, 8, 11, 14 — starts at 2, adds 3 each step, generates 5 terms. Validate step ≠ 0 and terms > 0. Implement __len__, __str__, and make it work with for, list(), sum() and next().

💡 Hint — iterable vs iterator mistake:

# WRONG — __iter__ doesn't reset state
def __iter__(self):
    return self    # no reset → second traversal gives empty

seq = ArithmeticSequence(2, 3, 5)
print(list(seq))    # → [2, 5, 8, 11, 14]
print(list(seq))    # → [] WRONG — exhausted

# RIGHT — __iter__ resets state each time
def __iter__(self):
    self.__current = self.__start       # reset to original value
    self.__remaining = self.__terms     # reset counter
    return self

When you do list(seq) Python calls iter(seq) which calls __iter__. If __iter__ doesn’t reset state, the second call finds the iterator exhausted. Save original values in __init__ and restore them in __iter__.

Final Challenge

Exercise 3 — Perfect numbers generator

A perfect number equals the sum of its proper divisors. 6 is perfect because 1 + 2 + 3 = 6. Write is_perfect(n), infinite perfects() generator, and perfects_up_to(limit) generator. Find all perfect numbers up to 10000 (there are four: 6, 28, 496, 8128).

💡 Hints: Find proper divisors by iterating from 1 to n//2. Use return inside the generator to signal the end — same pattern as primes_up_to in the practice article (link). The exhausted generator mistake appears if you try list(perfects_up_to(10000)) twice on the same object.

(Solutions same as Spanish version above)

Cheat sheet — Python iterators and generators

# ============================================
# CHEAT SHEET — Iterators and generators
# Sergio Learns · sergiolearns.com
# ============================================

# ITERABLE vs ITERATOR
# Iterable → has __iter__() → usable in for
# Iterator → has __iter__() and __next__() → produces elements one by one
# An iterator is always iterable but not the other way around

# BUILT-IN FUNCTIONS
it = iter(iterable)     # converts iterable to iterator
value = next(it)        # returns next element → StopIteration when done

# WHAT for ACTUALLY DOES
for x in obj: body
# equals:
it = iter(obj)
while True:
    try:
        x = next(it)
        body
    except StopIteration:
        break

# YOUR OWN ITERATOR
class MyIterator:
    def __init__(self, start, end, step=1):
        self.__start = start
        self.__end = end
        self.__step = step

    def __iter__(self):
        self.__current = self.__start    # RESET HERE — not in __init__
        return self

    def __next__(self):
        if self.__current > self.__end:
            raise StopIteration
        value = self.__current
        self.__current += self.__step
        return value

# GENERATOR — the pythonic way
def my_generator(start, end, step=1):
    current = start
    while current <= end:
        yield current    # pause + return + resume after
        current += step

# INFINITE GENERATOR
def infinite():
    n = 0
    while True:
        yield n
        n += 1

# RETURN IN A GENERATOR → StopIteration
def up_to(limit):
    for x in infinite():
        if x > limit:
            return    # raises StopIteration
        yield x

# yield vs return
# return → function ends, state gone
# yield  → function pauses, state persists, resumes with next()

# __iter__ AS GENERATOR IN A CLASS
class MyContainer:
    def __iter__(self):
        node = self.__first
        while node is not None:
            yield node.value    # new generator each call → multiple traversals
            node = node.next_node

# GENERATOR vs LIST
lst = [i * 2 for i in range(1_000_000)]   # ~8MB
gen = (i * 2 for i in range(1_000_000))   # a few bytes — generator expression

# CHAINING GENERATORS
def evens():
    n = 0
    while True:
        yield n
        n += 2

def evens_up_to(limit):
    for e in evens():
        if e > limit: return
        yield e

# COMMON MISTAKES

# 1. EXHAUSTED GENERATOR — can't restart
gen = my_generator(1, 5)
list(gen)    # → [1, 2, 3, 4, 5]
list(gen)    # → [] exhausted
# SOLUTION: create new generator my_generator(1, 5) again

# 2. __iter__ that doesn't reset → empty on second traversal
class Wrong:
    def __iter__(self):
        return self    # no reset
class Right:
    def __iter__(self):
        self.__current = self.__start    # reset
        return self

# 3. for over infinite generator without break → infinite loop
for n in infinite():
    print(n)    # never stops — always add break or condition

# 4. next() on exhausted iterator → uncaught StopIteration
it = iter([1])
next(it)    # → 1
next(it)    # → StopIteration — catch it if not inside a for

Publicaciones Similares

Un comentario

Deja una respuesta

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