Protocolo de Iteración en Python | Capítulo 33

En Python, el protocolo de iteración es el mecanismo fundamental que permite a los objetos comportarse como colecciones iterables, como listas o tuplas, en bucles como for. Este protocolo se basa en dos métodos mágicos clave: __iter__ y __next__. Al implementar estos métodos en tus clases, puedes crear iteradores personalizados que generen secuencias de valores de manera controlada y eficiente. Imagina que estás diseñando un dispensador de caramelos: __iter__ prepara el dispensador, y __next__ entrega un caramelo cada vez que lo pides, hasta que se agota. En este capítulo, exploraremos estos conceptos de forma profunda, paso a paso, para que domines cómo hacer que tus objetos sean iterables sin depender de generadores avanzados.

Entendiendo el Rol de los Iterables e Iteradores

Comencemos por lo básico: en Python, un iterable es cualquier objeto que puede ser recorrido elemento por elemento, como una lista o un string. Pero detrás de escena, lo que hace posible esta iteración es un iterador, que es un objeto que mantiene el estado de la iteración y produce valores uno a uno.

Piensa en una iterable como un libro: puedes leerlo página por página. El iterador sería el marcador que recuerda dónde te quedaste y te da la siguiente página cada vez. Para que una clase sea iterable, debe implementar el método __iter__, que devuelve un iterador. Y para que ese iterador funcione, debe tener __next__, que retorna el siguiente elemento o lanza una excepción StopIteration cuando no hay más.

No saltemos adelante. Asegúrate de entender esto: sin __iter__, tu objeto no se puede usar en un bucle for. Sin __next__, el iterador no sabe cómo avanzar. Vamos a construir esto paso a paso.

Implementando un Iterador Básico desde Cero

Para ilustrar, crearemos una clase simple llamada Contador que itera de un número inicial a uno final, como contar del 1 al 5. Empezaremos implementando __iter__ y __next__ en la misma clase, lo cual es común para iteradores simples.

Aquí va el código. Guarda esto en un archivo llamado contador.py y ejecútalo con python contador.py en tu terminal para ver el resultado.

class Contador:
    def __init__(self, inicio, fin):
        # Inicializamos el estado del iterador
        self.actual = inicio  # Valor actual en la iteración
        self.fin = fin        # Límite superior (no inclusivo)

    def __iter__(self):
        # __iter__ debe devolver el iterador mismo (self)
        # Esto prepara el objeto para la iteración
        return self

    def __next__(self):
        # __next__ devuelve el siguiente valor o lanza StopIteration
        if self.actual < self.fin:
            valor = self.actual  # Guardamos el valor actual
            self.actual += 1     # Avanzamos al siguiente
            return valor
        else:
            # No hay más elementos: detenemos la iteración
            raise StopIteration

# Ejemplo de uso
if __name__ == "__main__":
    for num in Contador(1, 5):
        print(num)  # Imprime: 1 2 3 4
Python

Examinemos esto con detalle. En __init__, configuramos el estado inicial: actual es donde empezamos, y fin es el tope. __iter__ simplemente retorna self, diciendo “yo mismo soy el iterador”. Luego, __next__ verifica si hemos llegado al fin: si no, devuelve el valor actual y lo incrementa; si sí, lanza StopIteration para que el bucle for sepa detenerse.

Prueba a ejecutarlo. Verás que itera perfectamente, como una lista [1, 2, 3, 4]. ¿Por qué funciona? Porque el bucle for en Python llama internamente a iter(objeto) (que invoca __iter__) para obtener el iterador, y luego repite next(iterador) (que invoca __next__) hasta que se agota.

Repito lo clave: __iter__ inicia el proceso, __next__ lo avanza. Sin uno, el otro no sirve. Esta es la esencia del protocolo.

Separando Iterable e Iterador para Mayor Flexibilidad

En el ejemplo anterior, la clase Contador es tanto iterable como iterador. Eso está bien para casos simples, pero a veces quieres que el iterable devuelva un iterador separado, permitiendo múltiples iteraciones independientes sobre el mismo objeto.

Imagina un álbum de fotos: el álbum es el iterable, y cada vez que lo “iteras”, obtienes un visor (iterador) que recorre las fotos sin afectar a otros visores. Vamos a refactorizar nuestro contador para esto.

class AlbumFotos:
    def __init__(self, fotos):
        # El iterable almacena los datos
        self.fotos = fotos  # Lista de fotos, por ejemplo ['foto1', 'foto2']

    def __iter__(self):
        # __iter__ devuelve un nuevo iterador independiente
        return IteradorAlbum(self.fotos)

class IteradorAlbum:
    def __init__(self, fotos):
        # Inicializamos el estado del iterador
        self.fotos = fotos
        self.indice = 0  # Posición actual

    def __next__(self):
        # __next__ devuelve la siguiente foto o detiene
        if self.indice < len(self.fotos):
            foto = self.fotos[self.indice]
            self.indice += 1
            return foto
        else:
            raise StopIteration

# Ejemplo de uso
if __name__ == "__main__":
    album = AlbumFotos(['foto1', 'foto2', 'foto3'])
    for foto in album:
        print(foto)  # Imprime: foto1 foto2 foto3

    # Podemos iterar de nuevo independientemente
    for foto in album:
        print(foto)  # Imprime lo mismo de nuevo
Python

Aquí, AlbumFotos implementa solo __iter__, que crea un nuevo IteradorAlbum cada vez. El iterador maneja __next__ y el estado (índice). Esto permite iterar el álbum múltiples veces sin resetear nada, ya que cada iteración usa un iterador fresco.

Nota los comentarios en el código: son claros y explican cada parte. Prueba a guardar esto en album.py y correr python album.py. Verás que puedes iterar el álbum cuantas veces quieras, siempre desde el principio.

Manejo de Errores y Casos Límite en Iteradores

No todo es perfecto. ¿Qué pasa si intentas iterar más allá del fin? Python lo maneja con StopIteration, pero tú debes asegurarte de lanzarla correctamente para evitar bucles infinitos.

Considera un caso límite: un iterable vacío. Si inicio >= fin en nuestro Contador__next__ lanzará StopIteration inmediatamente, y el bucle for no ejecutará nada. Eso es correcto.

Otro error común: olvidar retornar self en __iter__. Si lo haces, obtendrás un TypeError: iter() returned non-iterator. Siempre verifica eso.

Usa analogías para solidificar: es como un río (iterable) y un bote (iterador) que flota downstream, recogiendo items hasta el final. Si el río está seco, el bote se detiene de inmediato.

Practica modificando el código: agrega un paso de 2 en el contador, o itera hacia atrás. Experimenta para dominar.

Aplicaciones Prácticas del Protocolo de Iteración

Ahora que entiendes la mecánica, veamos por qué importa. Puedes crear iteradores para datos infinitos (con cuidado, para no bucles eternos), como un generador de números pares: solo detén con StopIteration cuando sea necesario.

O para procesar archivos grandes: itera línea por línea sin cargar todo en memoria. El protocolo es la base de eso.

Recuerda: no usamos generadores aquí (como yield), pero __iter__ y __next__ son la forma pura y manual de lograr lo mismo. Domínalos, y entenderás cómo Python hace la magia internamente.

Resumen del Capítulo

  • Conceptos clave: El protocolo de iteración se basa en __iter__ (devuelve un iterador) y __next__ (devuelve el siguiente elemento o lanza StopIteration).
  • Implementación básica: Crea una clase que sea tanto iterable como iterador, inicializando estado en __init__ y avanzando en __next__.
  • Separación para reutilización: Usa una clase iterable que devuelva un iterador separado, permitiendo múltiples iteraciones independientes.
  • Manejo de errores: Siempre lanza StopIteration al final; verifica casos vacíos y evita bucles infinitos.
  • Aplicaciones: Útil para secuencias personalizadas, procesamiento eficiente de datos y comprensión profunda de bucles en Python.
  • Práctica recomendada: Modifica los ejemplos para iterar sobre estructuras personalizadas, como un árbol o una base de datos simulada, para reforzar el dominio.

Dejar un comentario

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

Scroll al inicio