Iteradores en Python: __iter__ y __next__ para objetos iterables propios

En Python, casi todo lo que puedes recorrer con un bucle for es un iterable. Pero un iterable y un iterador no son lo mismo. Entender la diferencia y saber implementar el protocolo iterador te permite crear objetos que se integran de forma nativa con for, list(), sum() y cualquier función que acepte iterables.

Iterable vs iterador

# Una lista es iterable: tiene __iter__
nums = [1, 2, 3]
it = iter(nums)   # __iter__ devuelve un iterador

# El iterador tiene __next__
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3
# next(it)       # StopIteration — se agotó

# Puedes crear múltiples iteradores del mismo iterable
it1 = iter(nums)
it2 = iter(nums)
next(it1)  # 1
next(it2)  # 1 — independiente de it1

Implementar __iter__ y __next__ en una clase

class ContadorDescendente:
    """Itera de n hasta 1."""

    def __init__(self, inicio):
        self.inicio  = inicio
        self.actual  = inicio

    def __iter__(self):
        """Devuelve el propio objeto como iterador."""
        self.actual = self.inicio
        return self

    def __next__(self):
        if self.actual <= 0:
            raise StopIteration
        valor = self.actual
        self.actual -= 1
        return valor

cd = ContadorDescendente(5)
for n in cd:
    print(n, end=" ")  # 5 4 3 2 1

# Se puede reutilizar porque __iter__ reinicia el estado
print(list(cd))   # [5, 4, 3, 2, 1]

Separar iterable de iterador

Cuando el mismo objeto sirve como iterable e iterador (como arriba), tiene el problema de que compartir el objeto entre dos bucles interfieren. La solución es separar el estado de iteración en una clase aparte:

class RangoFibonacci:
    """Iterable de n números de Fibonacci."""

    def __init__(self, n):
        self.n = n

    def __iter__(self):
        return IteradorFibonacci(self.n)

class IteradorFibonacci:
    def __init__(self, n):
        self.n    = n
        self.a, self.b = 0, 1
        self.contador  = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.contador >= self.n:
            raise StopIteration
        valor = self.a
        self.a, self.b = self.b, self.a + self.b
        self.contador += 1
        return valor

fibs = RangoFibonacci(8)
print(list(fibs))   # [0, 1, 1, 2, 3, 5, 8, 13]
print(list(fibs))   # [0, 1, 1, 2, 3, 5, 8, 13] — sigue funcionando

Iterador infinito con islice

from itertools import islice

class Naturales:
    """Iterador infinito de números naturales."""

    def __init__(self):
        self.n = 0

    def __iter__(self):
        return self

    def __next__(self):
        self.n += 1
        return self.n

nat = Naturales()
print(list(islice(nat, 10)))   # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(next(nat))               # 11 — continúa donde se quedó

Alternativa con generador

La mayoría de las veces, un generador es más simple que implementar la clase iteradora completa:

def contador_descendente(n):
    while n > 0:
        yield n
        n -= 1

print(list(contador_descendente(5)))  # [5, 4, 3, 2, 1]

Implementa el protocolo iterador con clases cuando necesites estado complejo, reinicios controlados o compatibilidad con herencia. Usa generadores para todo lo demás.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP