34 · Generadores con yield
Escribe secuencias perezosas con yield — una función que produce valores bajo demanda, usa memoria constante independientemente de la longitud de la secuencia y se compone limpiamente.
Una **función generadora** (cualquier `def` con `yield`) devuelve un **objeto generador** cuando se llama — **no** ejecuta el cuerpo inmediatamente. Cada llamada a `next()` en el generador reanuda la ejecución desde donde `yield` la pausó por última vez, emitiendo un valor. Cuando la función retorna (o llega al final) Python lanza automáticamente `StopIteration`. El generador es un iterador — se conecta a `for`, `zip`, `list()` y todo lo demás.
Sin esto:
Sin generadores construirías listas gigantes para representar secuencias — un rango de un millón de elementos consumiría millones de bytes antes de que tu bucle siquiera comience. Los generadores permiten computación en streaming: el siguiente valor se calcula solo cuando se solicita, lo que es la base de todas las pipelines de datos eficientes en memoria.
Los generadores existen para resolver dos problemas a la vez:
- Memoria — si estás produciendo una secuencia larga, no hay razón para tener todos los valores en RAM a la vez. Un generador emite un valor, lo descarta y calcula el siguiente.
- Composición — los generadores se encadenan sin asignaciones intermedias.
gen_b(gen_a(fuente))extrae valores a través de una pipeline de transformaciones de a uno.
La sintaxis es simple: pon yield en cualquier lugar dentro de un def y la función se convierte en una función generadora. Llamarla devuelve un objeto generador; el cuerpo no ha corrido todavía. Cada llamada a next() reanuda el cuerpo hasta el siguiente yield, luego se suspende de nuevo. Hay tres formas de escribir generadores en Python: funciones generadoras (def + yield), expresiones generadoras ((...)), y yield from para delegación.
Python (in browser)
Llamar `counts_up_to(3)` no ejecuta ni una línea del cuerpo — el `for i in range(n)` no ha comenzado. Solo cuando se llama `next(g)` la ejecución entra en la función, corre hasta `yield i`, emite el valor y luego se congela.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
La expresión generadora `(x*x for x in range(N))` crea un objeto diminuto (alrededor de 200 bytes) sin importar qué tan grande sea `N`. La comprensión de lista equivalente asigna memoria para cada elemento de antemano. Usamos 1M aquí para que la celda corra cómoda en el navegador; el mismo generador con `range(1_000_000_000)` sería igual de pequeño. Para secuencias que solo consumes parcialmente, el generador es siempre la elección correcta.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
Python (in browser)
`yield from f` delega la iteración al propio objeto de archivo — cada llamada a `next()` lee exactamente una línea del disco sin almacenar todo el archivo en buffer. Este es el patrón idiomático para procesamiento de archivos en streaming en Python: envuelve la lógica de open/read en un generador y los callers nunca ven los handles de archivo.
Python runs entirely in your browser via Pyodide (~6 MB on first Run, cached after).
`yield from iterable` es una abreviatura de `for x in iterable: yield x` pero también enruta correctamente las llamadas `send()` y `throw()` a través de la cadena (uso avanzado). El patrón de pipeline aquí es el equivalente Python de las pipes de Unix: cada etapa es independiente, componible y de memoria constante.
¿Llamar a una función generadora ejecuta inmediatamente su cuerpo?
- Una función generadora es cualquier `def` que contiene `yield`. Llamarla devuelve un objeto generador — el cuerpo no corre hasta que se llama `next()`.
- Las expresiones generadoras `(expr for x in iterable)` son el equivalente en una línea de las funciones generadoras y usan memoria constante.
- `yield from iterable` delega a otro iterable, facilitando componer generadores en pipelines sin asignaciones intermedias.
- Una expresión generadora de 10M elementos ocupa ~200 bytes; la lista equivalente ocupa ~80 MB. Para secuencias consumidas una vez o parcialmente, prefiere el generador.
Los datasets en streaming que no caben en RAM se modelan como generadores: `def batches(ruta, tamaño_batch): ... yield batch` es el patrón detrás de `IterableDataset` de PyTorch y `tf.data.Dataset` de TensorFlow. HuggingFace `datasets.IterableDataset` es una API basada en generadores para datasets demasiado grandes para cachear localmente. Incluso `np.load(..., mmap_mode='r')` de NumPy devuelve un array mapeado en memoria que actúa como un generador perezoso sobre datos en disco.
Si lo quitas: Sin generadores, cargar un dataset de 100 GB para fine-tuning requeriría materializar todo el contenido en RAM antes de que el entrenamiento pueda comenzar — una restricción imposible en la mayoría de las máquinas. El modelo de generadores es lo que hace factible entrenar con datos que son órdenes de magnitud más grandes que la memoria disponible.