Fundamentos de Iterables, Iteradores y Generadores en Python

Objetos Iterables

En Python, cualquier estructura que pueda recorrerse mediante un bucle for se clasifica como iterable. Los tipos nativos como listas, tuplas, cadenas, conjuntos y diccionarios implementan esta capacidad de forma predeterminada. Para validar programáticamente si un elemento cumple con esta característica, existen dos enfoques comunes:

from collections.abc import Iterable

# Enfoque 1: Verificación de tipo en tiempo de ejecución
print(isinstance("cadena_de_texto", Iterable))  # True
print(isinstance(100, Iterable))                # False

# Enfoque 2: Inspección del atributo mágico
print(hasattr([10, 20], "__iter__"))            # True
print(hasattr({"a": 1}, "__iter__"))            # True

Intentar recorrerr un tipo que no soporta iteración, como un número entero o flotante, provocará inmediatamente un TypeError.

Construcción Ingenua de Secuencias

Una forma tradicional de generar datos consiste en acumularlos en una estrutcura en memoria antes de devolverlos. Este patrón ilustra el comportamiento de un iterable convencional, pero revela limitaciones de rendimiento con volúmenes altos:

def construir_secuencia(limite):
    """Genera una lista completa en memoria antes de retornar"""
    indice = 0
    contenedor = []
    while indice < limite:
        contenedor.append(indice)
        indice += 1
    return contenedor

# El resultado es un iterable válido
print(isinstance(construir_secuencia(5), Iterable))

# Advertencia: valores extremos en 'limite' bloquearán la ejecución 
# y consumirán RAM hasta agotar los recursos disponibles

Protocolo de Iteración

Un iterador es un objeto que administra el estado del recorrido y entrega elementos de uno en uno. Para que una clase actúe como iterador, debe satisfacer el protocolo de iteración de Python:

  1. Implementar __iter__() retornando la propia instancia (self).
  2. Implementar __next__() devolviendo el siguiente valor o lanzando StopIteration cuando no queden elementos.
from collections.abc import Iterator

rango_base = range(10)
flujo_datos = iter(rango_base)

# range es iterable, pero no es un iterador en sí mismo
print(isinstance(rango_base, Iterator))   # False
print(isinstance(flujo_datos, Iterator))  # True

# Verificación estructural
print("__next__" in dir(flujo_datos))     # True
print("__next__" in dir([1, 2, 3]))       # False

Generadores y la Instrucción Yield

Los generadores abstraen la complejidad del protocolo de iteración. Al introducir la palabra reservada yield dentro de una función, el intérprete la convierte automáticamente en una fábrica de generadores. Llamar a dicha función no ejecuta su cuerpo; únicamente instancia un objeto generador preparado para producir valores bajo demanda.

Comparativa de Implementaciones

A continuación se contrastan tres estrategias para calcular potencias cúbicas dentro de un intervalo numérico:

# Estrategia A: Clase iteradora explícita
class IteradorCubos:
    def __init__(self, inicio, fin):
        self.posicion = inicio
        self.limite = fin

    def __iter__(self):
        return self

    def __next__(self):
        if self.posicion > self.limite:
            raise StopIteration
        resultado = self.posicion ** 3
        self.posicion += 1
        return resultado

# Estrategia B: Función generadora
def generador_cubos(inicio, fin):
    for valor in range(inicio, fin + 1):
        yield valor ** 3

# Estrategia C: Expresión generadora
expr_cubos = (x ** 3 for x in range(1, 4))

print(type(generador_cubos(1, 3)))  # <class 'generator'>
print(type(expr_cubos))             # <class 'generator'>

Mecánica de Ejecución y Suspensión de Estado

La diferencia fundamental entre return y yield radica en el ciclo de vida del marco de ejecución. Mientras return finaliza la función y destruye su contexto local, yield pausa la rutina, preserva todas las variables en memoria y cede el control. La ejecución solo se reanuda cuando se invoca next() sobre el objeto generador.

def flujo_con_pausas():
    print("[Fase 1] Contexto inicializado")
    yield "Primer_valor"
    print("[Fase 2] Reanudación tras pausa")
    yield "Segundo_valor"
    print("[Fase 3] Cierre de rutina")

def flujo_tradicional():
    salida = []
    salida.append("Primer_valor")
    salida.append("Segundo_valor")
    return salida

# Instanciación y consumo manual
obj_gen = flujo_con_pausas()
print(type(obj_gen))          # <class 'generator'>
print(next(obj_gen))          # Ejecuta hasta el primer yield
print(next(obj_gen))          # Reanuda, imprime Fase 2 y cede el segundo valor
# Una tercera llamada a next() ejecutaría Fase 3 y lanzaría StopIteration

Este mecanismo de evaluación perezosa permite trabajar con flujos de datos continuos o colecciones masivas sin cargar la información compelta en la memoria principal, garantizando un consumo de recursos constante y predecible.

Etiquetas: Python iterables iterators generators Yield

Publicado el 8-11 09:43