Kodokon kodokon.com

Iteradores y generadores: yield y pipelines

Domina el protocolo de iteración y los generadores para procesar flujos de datos de forma perezosa, con memoria constante.

10 min · 3 preguntas

Abrir esta lección en Kodokon

El for de Python no es más que un protocolo: iter(obj) pide un iterador, después se llama a next() hasta la excepción StopIteration. Distingue bien el iterable (la lista, capaz de entregar un iterador nuevo en cada pasada) del iterador (el cursor, que solo se puede recorrer una vez). Entender este protocolo es entender por qué algunos objetos "se consumen".

PYTHON
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
# next(iterator) would raise StopIteration
Lo que hace un bucle for, hecho a mano

Una función que contiene yield no se ejecuta al llamarla: devuelve un generador, un iterador cuyo cuerpo se ejecuta bajo demanda. Cada next() avanza hasta el siguiente yield, donde la ejecución queda suspendida junto con todo su estado local. La consecuencia directa: la memoria se mantiene constante sea cual sea el tamaño del flujo - produces los valores uno a uno en lugar de materializar una lista completa.

PYTHON
def squares(limit):
    n = 0
    while n < limit:
        yield n * n
        n += 1

gen = squares(4)
print(next(gen))  # 0
print(list(gen))  # [1, 4, 9]
yield suspende la función entre dos valores

El beneficio principal en producción: los pipelines. Cada etapa consume el flujo de la anterior y produce el suyo, de forma perezosa; nada se ejecuta antes del consumo final (list(), sum(), un for). Un compromiso a tener en cuenta: una lista se puede recorrer varias veces y cortar en rebanadas, un generador no; en cambio, un generador procesa un archivo de 10 GB sin saturar la RAM. Las expresiones generadoras - (f(x) for x in xs if cond) - cubren las etapas sencillas sin definir una función.

PYTHON
lines = ["  12 ", "x", " 7", "", "30"]

def clean(rows):
    for row in rows:
        row = row.strip()
        if row.isdigit():
            yield int(row)

def keep_from(values, floor):
    return (v for v in values if v >= floor)

pipeline = keep_from(clean(lines), 10)
print(list(pipeline))  # [12, 30]
Un pipeline perezoso: nada se ejecuta antes de list()

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Qué obtienes al llamar a una función cuyo cuerpo contiene yield?
    • El resultado del primer yield
    • Un generador, sin ejecutar el cuerpo
    • Una lista con todos los valores producidos
  2. ¿Qué ventaja decisiva tiene un pipeline de generadores frente a las listas intermedias?
    • Los resultados se ordenan automáticamente
    • La memoria se mantiene constante: los valores se producen bajo demanda
    • El procesamiento se paraleliza en varios núcleos
  3. Después de un primer list(gen), ¿qué devuelve un segundo list(gen) sobre el mismo generador?
    • La misma lista que la primera llamada
    • Una lista vacía
    • Se propaga una excepción