CAP 12 · LEC 04·Python avanzado

Iteradores: protocolo __iter__ y __next__ desde cero

Todo lo que el for puede recorrer implementa el protocolo iterador: __iter__ retorna self, __next__ retorna el siguiente valor o lanza StopIteration. Entenderlo desmitifica buena parte de Python.

● AVANZADO10 min lectura4 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

El protocolo iterador: __iter__ y __next__

Python define el protocolo iterador con dos métodos: __iter__() que retorna el iterador (generalmente self) y __next__() que retorna el siguiente elemento o lanza StopIteration cuando se agota.

# Qué hace el for internamente mi_lista = [1, 2, 3] # Python convierte esto... for item in mi_lista: print(item) # ...en esto (internamente): iterador = iter(mi_lista) # llama __iter__() while True: try: item = next(iterador) # llama __next__() print(item) except StopIteration: break # Inspeccionar el protocolo print(hasattr(mi_lista, "__iter__")) # True — es iterable print(hasattr(mi_lista, "__next__")) # False — no es iterador por sí solo it = iter(mi_lista) print(hasattr(it, "__iter__")) # True print(hasattr(it, "__next__")) # True — el iterador sí tiene __next__ # next() con valor por defecto (no lanza StopIteration) it2 = iter([10, 20]) print(next(it2)) # 10 print(next(it2)) # 20 print(next(it2, None)) # None — agotado, sin excepción
Salida1 2 3 True False True True 10 20 None

Implementar un iterador custom

Implementar el protocolo desde cero revela exactamente qué sucede cuando Python itera sobre un objeto. El ejemplo clásico: un rango propio.

class CountUp: """Itera de start hasta stop (excluido), de step en step.""" def __init__(self, start: int, stop: int, step: int = 1): self.current = start self.stop = stop self.step = step def __iter__(self): return self # el objeto es su propio iterador def __next__(self): if self.current >= self.stop: raise StopIteration value = self.current self.current += self.step return value # Usar en un for for n in CountUp(0, 10, 2): print(n, end=" ") print() # 0 2 4 6 8 # Usar con list(), sum(), zip(), etc. print(list(CountUp(1, 6))) # [1, 2, 3, 4, 5] print(sum(CountUp(1, 101))) # 5050 # Un iterador se agota: no puede reiniciarse counter = CountUp(1, 4) print(list(counter)) # [1, 2, 3] print(list(counter)) # [] — ya agotado # Separar iterable de iterador para poder recorrer múltiples veces class CountUpIterable: """Iterable: puede generar múltiples iteradores.""" def __init__(self, start: int, stop: int): self.start = start self.stop = stop def __iter__(self): return CountUp(self.start, self.stop) # nuevo iterador cada vez nums = CountUpIterable(1, 4) print(list(nums)) # [1, 2, 3] print(list(nums)) # [1, 2, 3] — vuelve a funcionar
Salida0 2 4 6 8 [1, 2, 3, 4, 5] 5050 [1, 2, 3] [] [1, 2, 3] [1, 2, 3]
Iterable vs Iterador

Un iterable tiene __iter__() que retorna un nuevo iterador cada vez. Un iterador tiene __iter__() (retorna self) y __next__(). Los iteradores se agotan; los iterables no. Una lista es iterable pero no iterador; iter(lista) retorna un iterador.

Iterables vs iteradores — diferencias clave

La distinción entre iterable e iterador tiene consecuencias prácticas en el código cotidiano, especialmente al reutilizar colecciones.

# Iterables estándar: list, tuple, set, dict, str, range numeros = [1, 2, 3] # Puedes iterar múltiples veces print(sum(numeros)) # 6 print(list(numeros)) # [1, 2, 3] — sigue intacta # Iteradores: generadores, zip, map, filter, enumerate gen = (x ** 2 for x in range(5)) # generador = iterador print(list(gen)) # [0, 1, 4, 9, 16] print(list(gen)) # [] — se agotó # El bug clásico: usar el mismo iterador dos veces combined = zip([1, 2, 3], ["a", "b", "c"]) first_pass = list(combined) second_pass = list(combined) # vacío — el zip ya se agotó print(first_pass) # [(1, 'a'), (2, 'b'), (3, 'c')] print(second_pass) # [] # Detectar si algo es iterable o iterador from collections.abc import Iterator, Iterable print(isinstance([1, 2], Iterable)) # True print(isinstance([1, 2], Iterator)) # False print(isinstance(iter([1, 2]), Iterator)) # True print(isinstance(iter([1, 2]), Iterable)) # True (Iterator es subclase de Iterable) # Patrón seguro: convertir a lista si vas a reutilizar def process_twice(iterable): items = list(iterable) # materializa en memoria una vez total = sum(items) average = total / len(items) return total, average print(process_twice(x for x in range(1, 6))) # (15, 3.0)
Salida6 [1, 2, 3] [0, 1, 4, 9, 16] [] [(1, 'a'), (2, 'b'), (3, 'c')] [] True False True True (15, 3.0)

itertools — combinadores potentes

El módulo itertools provee herramientas para trabajar con iteradores de forma eficiente, sin materializar colecciones completas en memoria.

import itertools # chain: concatena múltiples iterables letras = itertools.chain("abc", "def", [1, 2, 3]) print(list(letras)) # ['a', 'b', 'c', 'd', 'e', 'f', 1, 2, 3] # islice: slice perezoso sobre cualquier iterable primeros_5 = itertools.islice(itertools.count(10), 5) # count es infinito print(list(primeros_5)) # [10, 11, 12, 13, 14] # groupby: agrupa elementos consecutivos con la misma clave datos = [("py", "Ana"), ("py", "Carlos"), ("js", "Diana"), ("js", "Eva"), ("go", "Felipe")] for lenguaje, grupo in itertools.groupby(datos, key=lambda x: x[0]): nombres = [nombre for _, nombre in grupo] print(f"{lenguaje}: {nombres}") # product: producto cartesiano for combo in itertools.product("AB", [1, 2]): print(combo, end=" ") print() # ('A', 1) ('A', 2) ('B', 1) ('B', 2) # combinations y permutations print(list(itertools.combinations("ABC", 2))) # [('A', 'B'), ('A', 'C'), ('B', 'C')] # takewhile y dropwhile nums = [1, 3, 5, 2, 4, 6] print(list(itertools.takewhile(lambda x: x % 2 != 0, nums))) # [1, 3, 5] — para al primer par print(list(itertools.dropwhile(lambda x: x % 2 != 0, nums))) # [2, 4, 6] — salta hasta el primer par
Salida['a', 'b', 'c', 'd', 'e', 'f', 1, 2, 3] [10, 11, 12, 13, 14] py: ['Ana', 'Carlos'] js: ['Diana', 'Eva'] go: ['Felipe'] ('A', 1) ('A', 2) ('B', 1) ('B', 2) [('A', 'B'), ('A', 'C'), ('B', 'C')] [1, 3, 5] [2, 4, 6]

Practica