Generator y yield en Python: procesar datos grandes sin gastar RAM
1. Resumen
Cuando procesas archivos de log de varios GB o streams de millones de registros desde una base de datos, escribir código que “carga todo en una lista y luego procesa” hará que tu programa consuma RAM sin control y posiblemente falle. Este artículo te ayudará a entender cómo funcionan los generator y yield, a compararlos con las listas normales, y a aplicarlos a problemas reales como leer archivos grandes o construir pipelines de procesamiento de datos. Después de leerlo, podrás escribir generators correctamente y sabrás cuándo NO deberías usarlos.
2. El problema con el enfoque tradicional
Supón que necesitas leer un archivo de log de 5GB y contar las líneas que contienen la palabra “ERROR”:
def count_errors_bad(filepath: str) -> int:
with open(filepath, "r") as f:
lines = f.readlines() # carga TODO el archivo en RAM
return sum(1 for line in lines if "ERROR" in line)
f.readlines() lee el contenido completo del archivo en una lista de strings en memoria. Con un archivo de 5GB, Python asignará ~5GB+ de RAM (más el overhead de los objetos) antes de empezar a contar. Si tu máquina solo tiene 4GB de RAM, el programa fallará con MemoryError o el sistema se colgará por swap.
El problema de fondo: solo necesitas procesar una línea a la vez, pero el código obliga a la máquina a mantener todas las líneas simultáneamente.
3. ¿Qué es un generator?
Un generator es un tipo especial de iterable: genera valores bajo demanda (evaluación lazy) en lugar de calcular y almacenar todo de antemano. En lugar de retornar una lista completa, un generator retorna un valor a la vez, y solo calcula el siguiente cuando se le solicita.
def count_errors_good(filepath: str) -> int:
with open(filepath, "r") as f:
return sum(1 for line in f if "ERROR" in line)
Aquí f (el objeto de archivo) por sí mismo es un iterador — lee una línea a la vez cuando el bucle lo solicita, sin cargar el archivo completo en RAM. La memoria utilizada es solo una línea a la vez, sin importar si el archivo tiene 5GB o 50GB.
4. La palabra clave yield: cómo crear una generator function
Puedes escribir tu propio generator usando yield dentro de una función. Cuando una función contiene yield, Python automáticamente la convierte en una generator function — llamarla no ejecuta el código inmediatamente, sino que retorna un objeto generator.
def read_large_file(filepath: str):
with open(filepath, "r") as f:
for line in f:
yield line.strip()
yield line.strip()pausa la función aquí, retornando el valorline.strip()hacia afuera.- La siguiente llamada a
next(), la función continúa ejecutándose desde donde se pausó, no se reinicia desde el principio. - El estado de la función (variables locales, posición del puntero del archivo) se mantiene entre cada
yield.
Pruébalo:
gen = read_large_file("access.log")
print(type(gen))
print(next(gen))
print(next(gen))
<class 'generator'>
127.0.0.1 - GET /index.html 200
127.0.0.1 - GET /style.css 200
Nota: la línea gen = read_large_file(...) no abre el archivo, no lee nada. El código interno solo se ejecuta cuando llamas a next(gen) por primera vez.
5. Comparación de RAM: generator vs lista
Para ver claramente la diferencia, usa el módulo sys para medir el tamaño de los objetos:
import sys
def squares_list(n: int) -> list[int]:
return [i * i for i in range(n)]
def squares_gen(n: int):
for i in range(n):
yield i * i
big_list = squares_list(1_000_000)
big_gen = squares_gen(1_000_000)
print(f"List size: {sys.getsizeof(big_list):,} bytes")
print(f"Generator size: {sys.getsizeof(big_gen):,} bytes")
List size: 8,448,728 bytes
Generator size: 208 bytes
Una lista con 1 millón de elementos ocupa casi 8.5MB, mientras que un generator es solo un objeto pequeño de 208 bytes — porque no almacena valores, solo almacena el estado para calcular el siguiente cuando sea necesario.
Nota: Un generator solo se puede recorrer una vez. Después de agotarse, llamar a
next()nuevamente lanzaráStopIteration. Si necesitas recorrer varias veces, debes crear un nuevo generator llamando la función de nuevo.
6. Generator expressions: sintaxis más compacta
Similar a las list comprehensions pero usando paréntesis en lugar de corchetes, tienes generator expressions — sin necesidad de definir una función separada:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_squares_gen = (n * n for n in numbers if n % 2 == 0)
print(sum(even_squares_gen))
220
Los paréntesis () en lugar de [] hacen que Python cree un generator en lugar de una lista inmediatamente. Esta forma es útil cuando solo necesitas usar los valores una vez, por ejemplo pasarlos directamente a sum(), max(), o any().
7. Construir pipelines de procesamiento de datos con generators
La fortaleza de los generators es poder encadenar múltiples pasos de procesamiento sin crear listas intermedias. Supón que necesitas: leer archivo CSV de órdenes → filtrar órdenes con status = "PAID" → calcular la suma de amount.
import csv
def read_orders(filepath: str):
with open(filepath, newline="") as f:
reader = csv.DictReader(f)
for row in reader:
yield row
def filter_paid(orders):
for order in orders:
if order["status"] == "PAID":
yield order
def extract_amount(orders):
for order in orders:
yield float(order["amount"])
def total_paid_amount(filepath: str) -> float:
orders = read_orders(filepath)
paid_orders = filter_paid(orders)
amounts = extract_amount(paid_orders)
return sum(amounts)
Cada función read_orders, filter_paid, extract_amount es un generator — no procesan todos los datos inmediatamente, sino que se “conectan” entre sí formando un pipeline. Cuando sum(amounts) llama a next(), el valor se extrae a través de cada paso: leer 1 fila CSV → revisar el status → convertir a float → acumular. En un momento dado, solo un registro existe en memoria, aunque el archivo CSV tenga 10 filas o 10 millones.
7.1. Probar el pipeline con pytest
import pytest
from pathlib import Path
@pytest.fixture
def orders_csv(tmp_path: Path) -> str:
content = (
"order_id,amount,status\n"
"1,100.0,PAID\n"
"2,50.0,PENDING\n"
"3,200.0,PAID\n"
)
file_path = tmp_path / "orders.csv"
file_path.write_text(content)
return str(file_path)
def test_total_paid_amount(orders_csv):
result = total_paid_amount(orders_csv)
assert result == pytest.approx(300.0)
tmp_path es una fixture integrada de pytest que crea un directorio temporal único para cada prueba. Esta prueba crea un archivo CSV simulado con 3 órdenes, solo 2 con estado PAID, y verifica que la suma sea exactamente 300.0.
8. yield from: delegación a otro generator
Cuando un generator necesita llamar y anidar resultados de otro generator, yield from ayuda a escribir código más compacto en lugar de escribir un bucle manual.
def read_file_lines(filepath: str):
with open(filepath) as f:
for line in f:
yield line.strip()
def read_multiple_files(filepaths: list[str]):
for filepath in filepaths:
yield from read_file_lines(filepath)
yield from read_file_lines(filepath) es equivalente a escribir for line in read_file_lines(filepath): yield line, pero es más compacto y maneja correctamente incluso el caso donde el generator interno retorna un valor. La función read_multiple_files te permite recorrer múltiples archivos de log como si fueran un stream continuo, sin necesidad de fusionar listas.
9. Enviar valores a un generator con send()
Los generators no solo generan valores hacia afuera — también puedes enviar valores hacia adentro usando send(), convirtiéndolos en corrutinas simples.
def running_average():
total = 0.0
count = 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
avg_gen = running_average()
next(avg_gen) # inicia el generator, ejecuta hasta el primer yield
print(avg_gen.send(10)) # envía 10, average = 10/1
print(avg_gen.send(20)) # envía 20, average = 30/2
print(avg_gen.send(30)) # envía 30, average = 60/3
10.0
15.0
20.0
La línea value = yield average hace dos cosas: retorna average hacia afuera, mientras simultáneamente espera recibir un valor a través de send() para asignarlo a value. Debes llamar a next(avg_gen) una primera vez para llevar el generator al primer punto yield — si llamas a send() inmediatamente sin haber iniciado, Python lanzará TypeError.
Nota: Usar
send()hace el código más difícil de leer que un generator normal. En la práctica, frameworks comoasyncioya han abstraído este mecanismo; rara vez necesitarás escribirsend()por tu cuenta a menos que estés construyendo una biblioteca.
10. Cuándo NO deberías usar generators
Los generators no siempre son mejores que las listas:
- Necesitas acceso aleatorio o recorrer múltiples veces: los generators no soportan
gen[5]o recorrer de nuevo desde el principio. Si necesitaslen(), slicing, o procesar datos 2-3 veces, una lista es más apropiada. - Los datos son pequeños, sin preocupaciones de RAM: con unos pocos miles de elementos, el overhead de gestionar el estado del generator a veces es más lento que una list comprehension porque las listas están optimizadas a nivel C.
- Necesitas paralelismo o dividir el trabajo entre múltiples workers: las listas se dividen fácilmente en
chunkspara enviar a multiprocessing; los generators deben “materializarse” parcialmente antes de dividirse. - El debugging es más difícil: imprimir una lista muestra inmediatamente todos los valores; imprimir un generator solo muestra
<generator object ... at 0x...>, y para ver los valores debes recorrerlo (y una vez recorrido, el generator se “agota” y no se puede reutilizar).
Si no estás seguro, la regla simple es: usa list cuando los datos caben en RAM y necesitas usarlos varias veces; usa generator cuando los datos son grandes, solo necesitas procesarlos secuencialmente una vez, o estás construyendo un pipeline de streaming.
11. Conclusión
Los generators y yield resuelven el problema correcto: procesar datos grandes sin cargar todo en RAM, gracias al mecanismo de evaluación lazy — calculando valores solo cuando se solicitan. Ya has visto cómo escribir generator functions, generator expressions, encadenar múltiples generators en un pipeline usando yield from, e incluso cómo enviar datos hacia atrás a un generator con send(). Recuerda el tradeoff: los generators ahorran memoria pero a cambio pierdes la capacidad de recorrer de nuevo, acceso aleatorio, y a veces el código es más difícil de leer. Elige la herramienta correcta para el trabajo correcto, no uses generators en todas partes solo porque “ahorran RAM”.