Pregunta 1¿Cuál es la mejor coincidencia para print(type(gen)) aquí?def f():
yield 1
yield 2
gen = f()
print(type(gen))
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)
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.
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.
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)
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.
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.
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 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.
Verificación de conocimientos
Responde cada pregunta una a una.
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))