Aprende leyendo en orden

Funciones generadoras con yield — Producir valores uno a uno para ahorrar memoria

Aprende las funciones generadoras de Python con yield para producir valores uno a uno y ahorrar memoria.

En el artículo anterior viste las clausuras — cómo sellar estado dentro de una función y devolver un valor distinto en cada llamada. Python tiene un mecanismo dedicado para este tipo de función «devolver el siguiente valor en cada llamada»: la función generadora. Usa yield en vez de return, y la función se pausa a mitad para retornar un valor, luego se reanuda desde donde se quedó la siguiente vez que se llama.

Encaja muy bien cuando no quieres materializar una colección grande de golpe y prefieres transmitir un valor a la vez — procesamiento de logs, cálculo masivo de datos y cargas de trabajo similares.

Fundamentos de yield — Devolver un valor a la vez

Una función con yield valor en su cuerpo es una función generadora. Llamarla como una función normal no ejecuta el cuerpo — solo recibes a cambio un objeto especial generator.

Para extraer un valor, llama a next(obj).

El primer next() ejecuta el cuerpo hasta el primer yield y retorna ese valor.

El siguiente next() reanuda desde ahí y ejecuta hasta el siguiente yield.

Una vez que no quedan más yields, otro next() lanza StopIteration.

def simple():
    yield 1
    yield 2

gen = simple()
print(type(gen))    # <class 'generator'>

print(next(gen))    # 1
print(next(gen))    # 2
print(next(gen))    # 3
# print(next(gen))  ← StopIteration (no quedan yields)
Cómo se relacionan yield y next()
gen = simple()(aún no se ejecuta)se crea el objeto generatornext(gen) → para en yield 1 → retorna 1next(gen) → para en yield 2 → retorna 2next() → no quedan yields, StopIteration1ª vez2ª vez3ª en adelante

Diferencia con return

Una función normal hace return y todo el ámbito se desvanece — la siguiente llamada empieza desde arriba. Una función generadora, en cambio, se pausa en yield y mantiene las locales y la posición. El siguiente next() reanuda justo donde lo dejaste, y esa es la gran diferencia.

Construye una función generadora que produzca IDs de pedido 1, 2, 3 en orden, y extráelos uno a uno con next().

① Define def order_ids(): con tres líneas: yield 1 / yield 2 / yield 3.

② Construye un objeto generator: gen = order_ids().

③ Llama a print(next(gen)) tres veces seguidas y confirma que salen 1 / 2 / 3.

(Cuando la respuesta sea correcta, aparecerá la explicación.)

Editor Python

Ejecutar el código para ver el resultado

Extraer valores con for — Sin preocuparse por StopIteration

Escribir next() cada vez se vuelve tedioso, y tampoco deberías tener que manejar StopIteration por tu cuenta. Usa for valor in generador: y Python llama a next() por debajo y sale del bucle automáticamente cuando el generador termina. Esta es, con diferencia, la forma más común.

Combinarlo con un bucle for
for v in gen:Lado del generadoryield retorna y se pausaLado del que llamaEl cuerpodel bucle usa el valorPara automáticamentecuando se acabanlos yieldsnextvalor

Si esparces líneas de print("en progreso...") en ambos lados, puedes ver cómo cada yield alterna la ejecución de un lado a otro — lado del generador → lado del que llama → lado del generador.

def count_up_to(max_value):
    print("iniciando el generador")
    for i in range(max_value):
        print(f"  antes de yield: {i}")
        yield i
        print(f"  después de yield: {i}")

for v in count_up_to(3):
    print(f"recibido: {v}")

# Flujo de salida:
# iniciando el generador
#   antes de yield: 0
# recibido: 0
#   después de yield: 0
#   antes de yield: 1
# ... (continúa)

Usa for para recibir datos de clientes desde un generador que los transmite uno a uno.

① Define def each_customer(): y haz un bucle con for name in ["Ana", "Carlos", "Lucía"]:, haciendo yield name.

② Extrae valores con for name in each_customer(): e imprime f"Próximo cliente: {name}".

Si los tres aparecen en orden, has terminado.

Editor Python

Ejecutar el código para ver el resultado

Diferencia con list — Reducir el uso de memoria

Cuando trabajas con valores de 0 a 999_999, una comprensión de lista materializa los 1.000.000 de enteros de golpe en memoria. Un generador, en cambio, mantiene solo el valor actual y calcula el siguiente bajo demanda. La lista acaba en el rango de varios MB; el objeto generator en sí ocupa solo unos cientos de bytes.

Huella de memoria de list frente a generator
list[0, 1, 2, ..., 999999]Varios MBcargados de golpeBien cuando necesitastodo el conjuntogenerator(i for i in range(...))Unos cientos de bytes(solo el elemento actual)Bien para transmitiruno a la vez
import sys

MAX = 10 ** 6

# List: carga todo en memoria de golpe
data_list = [i for i in range(MAX)]
print(sys.getsizeof(data_list))
# p. ej. 8000056 (~8 MB)

# Generator expression: solo el elemento actual
data_gen = (i for i in range(MAX))
print(sys.getsizeof(data_gen))
# p. ej. ~200 bytes

# El código del lado del que llama es idéntico
for v in data_gen:
    if v > 2:
        break
    print(v)
# 0
# 1
# 2

El atajo de la generator expression

Cambia los corchetes [ ... ] de una comprensión de lista por paréntesis ( ... ) y tienes una generator expression. (i for i in range(1_000_000)) te da el mismo efecto que una función generadora basada en def en una sola línea. Pásala directamente a sum() / max() / any() y compañía — funciona sin más.

Construye una generator expression (una comprensión de lista con paréntesis en lugar de corchetes) para algunos datos de precio, confirma el tipo con type(), y luego extrae valores uno a uno con for.

① Construye prices = (base * 100 for base in range(1, 6)). El truco es usar ( ) en vez de [ ].

② Imprime el tipo con print(type(prices)) y confirma que aparece <class 'generator'>.

③ Extrae valores con for p in prices: e imprime f"Precio: {p}".

Editor Python

Ejecutar el código para ver el resultado

Encadenar generadores con yield from

Cuando quieres que un generador reenvíe valores de otro generador tal cual, puedes escribir yield from sub_generador en una línea en lugar de for v in sub: yield v. Es útil cuando quieres fusionar varias fuentes de datos en un único generador.

Por ejemplo, con una función que transmite las ventas de la sucursal de Tokio y otra para Osaka, alinear yield from tokyo_sales() y yield from osaka_sales() le da al que llama un generador que parece un único flujo continuo.

def tokyo_sales():
    yield 1200
    yield 980

def osaka_sales():
    yield 850
    yield 1340

def all_sales():
    yield from tokyo_sales()
    yield from osaka_sales()

for amount in all_sales():
    print(amount)
# 1200
# 980
# 850
# 1340
yield from delega en un sub-generador
Lado del que llamafor amountin all_sales()all_sales()generadorprincipalOrden recibido1200 → 980→ 850 → 1340①yield fromtokyo_sales()tokyo_sales()yield 1200yield 980②yield fromosaka_sales()osaka_sales()yield 850yield 1340impulsadelegarsiguiente al terminardelegar

yield from sub_gen() es abreviatura de for v in sub_gen(): yield v.

Los valores que el sub produce van directos al que llama externo, así

el 1200, 980 de tokyo_sales,

luego el 850, 1340 de osaka_sales

llegan a for amount in all_sales(): en orden.

Construye un generador que combine listas de inventario por tienda en un único flujo.

① Define def store_a(): y for item in ["manzana", "naranja"]: yield item.

② Define def store_b(): y for item in ["banana", "uva", "fresa"]: yield item.

③ Define def all_items(): y escribe yield from store_a() seguido de yield from store_b().

④ Itera con for item in all_items(): print(item) para imprimir los 5 en orden.

Editor Python

Ejecutar el código para ver el resultado
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1¿Cuál es la mejor coincidencia para print(type(gen)) aquí?
def f():
yield 1
yield 2
gen = f()
print(type(gen))

Pregunta 2¿Qué pasa cuando llamas a next() sobre un generador después de haber consumido todos los yield?

Pregunta 3¿Cuál de estas dos líneas usa drásticamente menos memoria?
A: data = [i for i in range(10**6)]
B: data = (i for i in range(10**6))