iteradores y generadores en Python práctica Fibonacci primos LinkedList

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.

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.


Python iterators and generators — 3 real programs that make them tangible

In the previous article we covered the theory, what an iterable is, how iter() and next() work under the hood, and why yield pauses instead of ending. Now we write three real programs where iterators and generators aren’t an academic exercise they’re the most natural tool for the job.

Program 1 — Infinite Fibonacci generator

Fibonacci is the perfect example for an infinite generator, each number depends on the previous one, the sequence never ends, and in practice you only need the first N numbers or those that meet some condition. With a list you’d have to decide upfront how many to generate. With a generator you simply ask for what you need.

def fibonacci():
    """Infinite Fibonacci sequence generator."""
    previous = 0
    current = 1

    while True:
        yield previous
        previous, current = current, previous + current

# First 10 Fibonacci numbers
gen = fibonacci()
for i in range(10):
    print(next(gen), end=' ')
print()
# → 0 1 1 2 3 5 8 13 21 34

# Fibonacci up to a limit
for n in fibonacci():
    if n > 100:
        break
    print(n, end=' ')
print()
# → 0 1 1 2 3 5 8 13 21 34 55 89

# Even Fibonacci numbers up to 500
for n in fibonacci():
    if n > 500:
        break
    if n % 2 == 0:
        print(n, end=' ')
print()
# → 0 2 8 34 144

Key line: previous, current = current, previous + current. Python evaluates the entire right side before assigning, no temporary variable needed.

The generator doesn’t know how many numbers you’ll need and doesn’t care. You decide from outside when to stop — with range(), break, or a condition.

Program 2 — Prime number generator

def is_prime(n):
    """Check if n is prime."""
    if n < 2: return False
    if n == 2: return True
    if n % 2 == 0: return False
    i = 3
    while i * i <= n:
        if n % i == 0: return False
        i += 2
    return True


def primes():
    """Infinite prime number generator."""
    yield 2
    candidate = 3
    while True:
        if is_prime(candidate):
            yield candidate
        candidate += 2


def primes_up_to(limit):
    """Generator of primes up to a given limit."""
    for p in primes():
        if p > limit:
            return    # return in generator → StopIteration
        yield p


def first_n_primes(n):
    """Generator of the first n primes."""
    count = 0
    for p in primes():
        if count >= n:
            return
        yield p
        count += 1


print("First 15 primes:")
for p in first_n_primes(15):
    print(p, end=' ')
print()
# → 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47

print("\nPrimes up to 50:")
for p in primes_up_to(50):
    print(p, end=' ')
print()

total = sum(1 for _ in primes_up_to(100))
print(f"\nPrimes up to 100: {total}")
# → 25

Important detail: return inside a generator raises StopIteration, the correct signal for the for loop to stop. Notice how first_n_primes and primes_up_to are generators that consume the primes() generator internally, you can chain generators. Each one consumes the previous element by element, loading nothing extra into memory.

Program 3 — Iterator for a LinkedList

Important note: This program uses a LinkedList: a linked data structure that is the last block of the FP2 syllabus. If you haven’t reached that topic in class yet, save this program for when you do — everything you need to know about iterators is already in the two programs above.

class LinkedList:
    class Node:
        def __init__(self, value, next_node=None):
            self.value = value
            self.next_node = next_node

    def __init__(self):
        self.__first = None
        self.__length = 0

    def append(self, value):
        new = self.Node(value)
        if self.__first is None:
            self.__first = new
        else:
            current = self.__first
            while current.next_node is not None:
                current = current.next_node
            current.next_node = new
        self.__length += 1

    def __len__(self):
        return self.__length

    def __iter__(self):
        """Makes LinkedList iterable using a generator."""
        current = self.__first
        while current is not None:
            value = current.value
            current = current.next_node
            yield value

    def __str__(self):
        return ' → '.join(str(v) for v in self) + ' → None'

    def __repr__(self):
        return f'LinkedList({list(self)})'


lst = LinkedList()
for v in [10, 20, 30, 40, 50]:
    lst.append(v)

print(f"List: {lst}")
# → 10 → 20 → 30 → 40 → 50 → None

for value in lst:
    print(value, end=' ')
print()
# → 10 20 30 40 50

print(f"Sum: {sum(lst)}")
print(f"Max: {max(lst)}")
print(f"As list: {list(lst)}")

# Multiple traversals work because __iter__ creates a new generator each time
print(list(lst))    # → [10, 20, 30, 40, 50]
print(list(lst))    # → [10, 20, 30, 40, 50]

The key difference from the simple iterator in the theory article: here __iter__ is a generator that creates a new object every time it’s called. You can traverse the list as many times as you want and it works correctly.

The pattern to take away

# PATTERN 1 — infinite generator with while True
def infinite_sequence():
    state = initial_value
    while True:
        yield computed_value
        update_state()

# PATTERN 2 — finite generator with return as end signal
def finite_sequence(limit):
    for element in infinite_sequence():
        if end_condition:
            return    # raises StopIteration
        yield element

# PATTERN 3 — __iter__ as generator in a class
class MyContainer:
    def __iter__(self):
        node = self.__first
        while node is not None:
            yield node.value
            node = node.next_node

Publicaciones Similares

Un comentario

Deja una respuesta

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