Curso de Python / Procesar datos bajo demanda con iteradores y generadores

Un iterador entrega un elemento cada vez que se solicita el siguiente; un generador permite programar ese comportamiento con yield. Los valores pueden calcularse bajo demanda, pero un iterador agotado no vuelve automáticamente al principio.

Diferenciar iterable, iterador y generador

Una lista es iterable: permite obtener un iterador mediante iter(lista). El iterador conserva la posición del recorrido. next solicita un valor y lanza StopIteration cuando ya no queda ninguno; un bucle for gestiona esa señal de finalización.

Llamar a una función que contiene yield crea un generador. El cuerpo avanza cuando se solicitan elementos y queda suspendido en cada yield, conservando el estado local necesario para continuar. La creación del generador y la ejecución completa de sus cálculos son momentos diferentes.

Consumir totales acumulados paso a paso

Guarda el siguiente programa en generators.py y ejecútalo con python generators.py. Utiliza solo la biblioteca estándar y una lista de tres importes.

def running_totals(amounts):
    total = 0
    for amount in amounts:
        total += amount
        yield total

values = running_totals([3500, 8000, 2500])
print(next(values))
print(list(values))
print(list(values))
print(list(running_totals([3500, 8000, 2500])))
3500
[11500, 14000]
[]
[3500, 11500, 14000]

La primera llamada consume el total 3500. La primera conversión a lista recoge únicamente los valores restantes, 11500 y 14000. La conversión siguiente encuentra el generador agotado y produce una lista vacía. Para obtener los tres resultados es necesario crear un generador nuevo, como hace la última línea.

Entender el coste real de la evaluación diferida

Producir un resultado cada vez evita almacenar de antemano toda la salida. No elimina la memoria de una lista de entrada que ya existe. Además, list(generador) vuelve a reunir todos los resultados en memoria. Si necesitas reutilizar una salida pequeña varias veces, guardarla en una lista puede ser más claro.

Ejecuta este segundo archivo como python iterator.py para comparar la posición del iterador con la conservación de la lista original:

values = [1, 2, 3]
iterator = iter(values)
print(next(iterator))
print(list(iterator))
print(list(iterator))
print(list(values))
expression = (n * 2 for n in values)
print(list(expression))
1
[2, 3]
[]
[1, 2, 3]
[2, 4, 6]

El iterador se agota, pero values sigue conteniendo sus tres elementos. La expresión generadora entre paréntesis calcula los dobles al consumirla. La evaluación diferida cambia cuándo se calcula y almacena cada valor; no garantiza por sí misma una ejecución más rápida. Herramientas como itertools también trabajan con estos recorridos.

Prever errores tardíos y recursos abiertos

Una excepción dentro del cuerpo de un generador puede aparecer durante su consumo, en vez de al crearlo. Coloca el manejo de errores alrededor de la operación que realmente recorre los elementos. Si el generador utiliza un archivo, decide quién lo cierra cuando el consumidor abandona el recorrido antes de terminar; un with controlado por quien llama puede hacer esa duración más evidente.

  1. Añade otra llamada a next antes de recoger el resto y observa cómo se reduce la salida pendiente.
  2. Prueba una entrada vacía y captura StopIteration al solicitar el primer elemento.
  3. Restaura el programa y crea dos generadores nuevos para hacer dos recorridos independientes.

La gestión de archivos con with explica el cierre de recursos, y la documentación oficial de generadores describe el estado que conserva yield.