⏱️ Lectura: 14 min

Una lista de un millón de números carga el millón completo en memoria antes de que puedas usar el primero. Una función con yield no: entrega un valor, se congela ahí mismo y solo continúa cuando alguien se lo pide. Esa pausa controlada es la base de los generadores en Python, uno de los mecanismos más usados y menos entendidos del lenguaje.

📑 En este artículo
  1. TL;DR
  2. Qué son los generadores en Python y por qué importan
  3. Cómo funciona yield en detalle
  4. Ejemplos prácticos: de lo simple a lo real
    1. Generador mínimo
    2. Fibonacci sin límite
    3. Leer un archivo grande línea por línea
    4. Expresión generadora: la versión compacta
  5. Cómo empezar: convertir una función en generador paso a paso
  6. Casos de uso reales
  7. Errores comunes y buenas prácticas
  8. Comparativa: lista, expresión generadora y función con yield
  9. Profundizando: send(), yield from y generadores asíncronos
  10. Preguntas frecuentes
    1. ¿Qué diferencia hay entre yield y return?
    2. ¿Un generador consume menos memoria siempre?
    3. ¿Puedo convertir un generador en lista?
    4. ¿Qué es una expresión generadora?
    5. ¿Cuándo conviene un generador asíncrono?
    6. ¿Los generadores son seguros entre hilos?
  11. Referencias

Este artículo explica qué hace exactamente la palabra clave yield, cómo funcionan los generadores en Python por dentro, y cuándo conviene usar uno en vez de una lista o una función normal.

TL;DR

  • Entenderás cómo yield pausa una función y la reanuda exactamente donde quedó, sin perder su estado.
  • Vas a poder construir generadores propios para leer archivos enormes línea por línea sin cargarlos completos en RAM.
  • Sabrás usar next() y send() para controlar manualmente un generador y el protocolo de iteración.
  • Vas a distinguir cuándo conviene una lista, una expresión generadora o una función con yield.
  • Aprenderás a evitar el error de recorrer un generador agotado y por qué solo se puede usar una vez.
  • Vas a conocer yield from para delegar en subgeneradores y simplificar pipelines de datos.
  • Entenderás la diferencia entre un generador síncrono y uno asíncrono con async def y async for.

Qué son los generadores en Python y por qué importan

Un generador es una función que, en lugar de devolver un único resultado con return y terminar, va entregando valores uno a la vez con yield y se suspende entre cada entrega. La función no vuelve a ejecutarse desde el principio: retoma exactamente en la línea siguiente al último yield, con todas sus variables locales intactas.

La diferencia práctica es memoria y tiempo. Una función que arma una lista con return tiene que calcular y guardar todos los elementos antes de devolver algo. Un generador equivalente con yield produce el primer elemento de inmediato y calcula el resto solo si alguien lo pide. Esto se llama evaluación perezosa (lazy evaluation).

Los generadores en Python existen desde la versión 2.2, definidos en la PEP 255: Simple Generators. Hoy son la base de módulos completos de la librería estándar, como itertools, y del propio protocolo de iteración que usa cualquier bucle for.

flowchart TD
    A["Funcion con return []"] --> B["Construye la lista completa"]
    B --> C[("Toda la lista en RAM")]
    D["Funcion con yield"] --> E["Genera un valor a la vez"]
    E --> F[("Solo el valor actual en RAM")]
Código Python con un generador usando yield
Un generador entrega valores uno a uno, sin construir toda la secuencia. Foto de Markus Spiske en Unsplash

Cómo funciona yield en detalle

Cuando Python encuentra un yield dentro del cuerpo de una función, esa función deja de ser una función normal: se convierte en una función generadora. Llamarla no ejecuta ni una línea de su cuerpo; solo crea y devuelve un objeto generador.

def contar_hasta(n):
    print("arrancando el generador")
    i = 1
    while i <= n:
        yield i
        i += 1

g = contar_hasta(3)
print(g)

Ejecutar este código imprime algo como <generator object contar_hasta at 0x...>. El print("arrancando el generador") todavía no se ejecutó: llamar a contar_hasta(3) solo crea el objeto generador, no corre el cuerpo de la función.

El cuerpo recién arranca cuando alguien pide el primer valor con la función next():

print(next(g))  # arrancando el generador -> 1
print(next(g))  # 2
print(next(g))  # 3
print(next(g))  # StopIteration

Cada llamada a next(g) corre el cuerpo hasta el siguiente yield, entrega ese valor y se suspende ahí. Cuando la función termina, Python lanza automáticamente la excepción StopIteration, la misma que un bucle for usa internamente para saber cuándo parar.

💭 Clave: un bucle for nunca llama a next() a mano: internamente hace exactamente next(iterador) en cada vuelta y sale cuando captura StopIteration. Los generadores no son un caso especial, son el mecanismo que hace funcionar a for.
stateDiagram-v2
    [*] --> Creado
    Creado --> Suspendido : primer next()
    Suspendido --> Ejecutando : next() o send()
    Ejecutando --> Suspendido : yield entrega un valor
    Ejecutando --> Agotado : termina la funcion
    Agotado --> [*]

Ejemplos prácticos: de lo simple a lo real

Generador mínimo

El generador más simple posible entrega un solo valor y termina:

def saludo():
    yield "hola desde un generador"

for mensaje in saludo():
    print(mensaje)

El bucle for llama a next() por vos, imprime el mensaje y, en la segunda vuelta, recibe StopIteration y sale del bucle sin lanzar error.

Fibonacci sin límite

Un generador puede representar una secuencia infinita porque nunca la calcula toda de una vez:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

fib = fibonacci()
primeros_10 = [next(fib) for _ in range(10)]
print(primeros_10)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Con una lista esto sería imposible: while True nunca termina, así que una lista jamás se completaría. Con yield, cada número existe solo en el instante en que se pide.

Leer un archivo grande línea por línea

El caso de uso más común en producción es procesar archivos que no entran en memoria:

def lineas_que_contienen(ruta, palabra):
    with open(ruta, encoding="utf-8") as archivo:
        for linea in archivo:
            if palabra in linea:
                yield linea.strip()

for coincidencia in lineas_que_contienen("logs.txt", "ERROR"):
    print(coincidencia)

Esta función nunca tiene el archivo completo en memoria. Aunque logs.txt pese varios gigabytes, el generador solo mantiene una línea a la vez, porque iterar el objeto archivo con for linea in archivo ya es, en sí mismo, perezoso.

Expresión generadora: la versión compacta

Cuando la lógica es simple, no hace falta definir una función completa. Una expresión generadora hace lo mismo entre paréntesis en vez de corchetes:

cuadrados = (x * x for x in range(1_000_000))
suma = sum(cuadrados)
print(suma)

Cambiar los paréntesis () por corchetes [] convertiría esto en una lista de un millón de enteros construida en memoria antes de sumar nada. Con paréntesis, sum() pide un valor a la vez y nunca hay más de un entero calculado en simultáneo.

💡 Tip: si solo necesitás recorrer los datos una vez y la lógica es simple, preferí una expresión generadora antes que definir una función completa con yield: menos código para el mismo resultado.
Diagrama de flujo de datos procesados con generadores en Python
Cada elemento se calcula solo cuando el consumidor lo pide. Foto de Lavi Perchik en Unsplash

Cómo empezar: convertir una función en generador paso a paso

Para pasar de una función normal a un generador no hace falta instalar nada; yield es parte del lenguaje desde Python 2.2. Los pasos son siempre los mismos:

  1. Identificá dónde tu función arma una lista con return, por ejemplo resultado.append(x) seguido de return resultado.
  2. Borrá la lista intermedia (resultado = [] y el .append).
  3. Reemplazá cada resultado.append(x) por yield x.
  4. Borrá el return resultado final; un generador termina solo cuando la función termina de ejecutarse.
# antes: construye toda la lista en memoria
def pares_hasta(n):
    resultado = []
    for x in range(n):
        if x % 2 == 0:
            resultado.append(x)
    return resultado

# despues: entrega los pares uno a uno
def pares_hasta_gen(n):
    for x in range(n):
        if x % 2 == 0:
            yield x

Para consumir cualquiera de las dos versiones el código que las llama no cambia: ambas funcionan dentro de un for, con list(...) o con sum(...). Podés confirmar que pares_hasta_gen es un generador con:

import types
print(isinstance(pares_hasta_gen(10), types.GeneratorType))  # True

Casos de uso reales

Los generadores aparecen en cualquier lugar donde el volumen de datos es grande, desconocido de antemano o potencialmente infinito:

  • Pipelines de datos: encadenar varios generadores, leer archivo, filtrar, transformar y agregar, procesa cada fila una sola vez sin materializar resultados intermedios completos.
  • Paginación de APIs: un generador puede pedir la página siguiente de una API solo cuando el consumidor pide el próximo elemento, sin traer de antemano todas las páginas.
  • ORMs y querysets: frameworks como Django devuelven objetos que se comportan como generadores para no cargar toda una tabla de base de datos en RAM al iterarla.
  • itertools: el módulo itertools de la librería estándar está construido casi enteramente sobre generadores, con funciones como itertools.count(), itertools.chain() e itertools.islice().
  • Streaming de eventos: sistemas que leen un socket o una cola de mensajes usan generadores para procesar eventos a medida que llegan, sin esperar a tener todos los eventos.

Errores comunes y buenas prácticas

El error más frecuente es intentar recorrer un generador dos veces. A diferencia de una lista, un generador es de un solo uso:

numeros = (x for x in range(3))
print(list(numeros))  # [0, 1, 2]
print(list(numeros))  # [] , ya esta agotado

Si necesitás recorrer los mismos datos varias veces, la solución no es arreglar el generador: es decidir si en ese caso conviene directamente una lista, o volver a crear el generador desde cero llamando otra vez a la función.

⚠️ Ojo: llamar a next() sobre un generador ya agotado no reinicia nada, sigue lanzando StopIteration para siempre. No hay forma de rebobinar un generador.

Otro error común es mezclar return valor dentro de un generador esperando que ese valor salga por un for. En un generador, return valor no entrega ese valor al que itera: solo termina la función, y ese valor queda disponible como atributo .value de la excepción StopIteration, algo pensado específicamente para yield from, no para uso normal.

Buenas prácticas: nombrá las funciones generadoras de forma que se note que devuelven una secuencia perezosa, evitá abrir recursos costosos fuera del generador si puede no consumirse nunca, y usá with dentro del cuerpo del generador para que el recurso se cierre incluso si el consumidor deja de iterar a mitad de camino.

Comparativa: lista, expresión generadora y función con yield

OpciónCuándo usarlaVentajaLimitación
ListaDatos pequeños que se recorren varias vecesSe puede indexar, recorrer N veces y usar len()Todo el contenido vive en memoria a la vez
Expresión generadoraUna sola pasada sobre datos grandes, lógica simpleSintaxis compacta y perezosaSe agota tras un uso, no admite indexado
Función con yieldLógica compleja, múltiples pasos o estado entre iteracionesPerezosa y reutilizable como fábrica de generadoresCada llamada crea un generador nuevo, no comparte estado entre llamadas
Generador asíncronoDatos que llegan de I/O asíncrona, sockets o APIsNo bloquea el event loop mientras espera datosSolo se recorre con async for, no con for normal

Profundizando: send(), yield from y generadores asíncronos

Un generador no solo entrega valores hacia afuera: también puede recibir valores desde afuera con send(). Esto se agregó en la PEP 342: Coroutines via Enhanced Generators, incluida en Python 2.5, y convirtió a los generadores en la base de las corrutinas antes de que existiera async/await.

def acumulador():
    total = 0
    while True:
        valor = yield total
        total += valor

acc = acumulador()
next(acc)             # arranca el generador, entrega total=0
print(acc.send(10))   # total ahora es 10, entrega 10
print(acc.send(5))    # total ahora es 15, entrega 15

Acá yield total hace dos cosas a la vez: entrega total hacia afuera y, cuando el generador se reanuda, esa misma expresión se evalúa al valor que llegó por send(). Esa doble vía es lo que distingue a yield como expresión de yield como sentencia simple.

sequenceDiagram
    participant Main as Programa principal
    participant Gen as Generador
    Main->>Gen: next(acc)
    Gen-->>Main: yield total = 0
    Main->>Gen: acc.send(10)
    Gen-->>Main: yield total = 10
    Main->>Gen: acc.send(5)
    Gen-->>Main: yield total = 15

Cuando un generador necesita delegar trabajo a otro generador, yield from evita escribir un bucle manual de reenvío. Se agregó en la PEP 380, parte de Python 3.3:

def generador_interno():
    yield 1
    yield 2

def generador_externo():
    yield from generador_interno()
    yield 3

print(list(generador_externo()))  # [1, 2, 3]

Por último, Python extendió el mismo concepto al mundo asíncrono con la PEP 525: Asynchronous Generators, disponible desde Python 3.6. Un generador asíncrono se define con async def y se recorre con async for en vez de for:

import asyncio

async def leer_paginas(cliente, url):
    pagina = 1
    while True:
        datos = await cliente.get(f"{url}?page={pagina}")
        if not datos:
            break
        yield datos
        pagina += 1

async def main():
    async for pagina in leer_paginas(cliente, "https://api.ejemplo.com/items"):
        print(pagina)

La diferencia con un generador normal es que cada yield puede estar precedido por un await que cede el control al event loop mientras espera una respuesta de red, sin bloquear el resto del programa.

Para confirmar en la práctica qué tipo de objeto tenés delante, el módulo inspect ofrece funciones específicas:

import inspect
print(inspect.isgeneratorfunction(fibonacci))    # True
print(inspect.isasyncgenfunction(leer_paginas))  # True

📖 Resumen en Telegram: Ver resumen

Tu próximo paso: abrí una terminal de Python, escribí un generador que lea un archivo de texto propio línea por línea y filtre solo las que contengan una palabra clave, y confirmá con isinstance(generador, types.GeneratorType) que efectivamente es perezoso.

Preguntas frecuentes

¿Qué diferencia hay entre yield y return?

Return termina la función y entrega un único valor final. Yield entrega un valor y suspende la función sin terminarla, dejando todas las variables locales intactas para la próxima llamada a next().

¿Un generador consume menos memoria siempre?

Consume menos memoria cuando no necesitás todos los valores a la vez o cuando la secuencia es muy grande o infinita. Si vas a recorrer los mismos datos varias veces o necesitás indexarlos, una lista suele ser más práctica.

¿Puedo convertir un generador en lista?

Sí, envolviéndolo con list(generador). Eso consume el generador por completo y guarda todos sus valores en memoria, perdiendo la ventaja de la evaluación perezosa.

¿Qué es una expresión generadora?

Es la versión compacta de un generador, escrita entre paréntesis en vez de corchetes, por ejemplo (x*x for x in datos). Se comporta igual que una función con yield pero sin necesidad de definir una función aparte.

¿Cuándo conviene un generador asíncrono?

Cuando cada valor depende de una operación de entrada/salida que tarda, como una llamada de red o una lectura de base de datos, y no querés bloquear el resto del programa mientras esperás esa respuesta.

¿Los generadores son seguros entre hilos?

No están pensados para compartirse entre hilos: un generador mantiene un único punto de ejecución, y llamar a next() desde varios hilos al mismo tiempo produce comportamiento indefinido. Cada hilo debería tener su propio generador.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Imagen destacada: Foto de Safar Safarov en Unsplash


Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.