Hace años, en un proyecto, me tocó procesar un archivo gigante, un CSV con millones de líneas. Mi primera idea, la ingenua, fue leerlo todo de golpe, parsearlo y guardarlo en una lista. Obviamente, el programa explotó por falta de memoria RAM. Ahí fue cuando me quemé de verdad y entendí que hay formas más inteligentes de trabajar con datos voluminosos. Y en Python, una de esas herramientas es yield.
Para entender qué hace yield, primero tenemos que aclarar un par de conceptos.
Iterables y Generadores: La base
Imagínate un iterable como una lista en Python. Puedes recorrer sus elementos uno por uno usando un for. Esto es iterar. La gracia de una lista es que todos sus elementos ya están creados y guardados en memoria. Si tienes pocos elementos, es perfecto. Pero con millones, ya sabes lo que pasa.
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
Ahora, los generadores son un tipo especial de iterable. La diferencia clave es que no almacenan todos los valores en memoria. En su lugar, generan los valores sobre la marcha, uno por uno, solo cuando los necesitas. Una vez que un valor ha sido generado y usado, el generador lo "olvida" y se prepara para generar el siguiente. Esto los hace increíblemente eficientes en cuanto a memoria.
Un ejemplo simple de cómo obtener un generador es con una expresión generadora, que se parece a una list comprehension pero usa paréntesis en vez de corchetes:
>>> mygenerator = (x*x for x in range(3)) # Esto crea un generador
>>> for i in mygenerator:
... print(i)
0
1
4
Ojo que si intentas iterar mygenerator una segunda vez, no obtendrás nada, porque ya generó todos sus valores y terminó. Es de un solo uso.
¿Y qué hace 'yield'?
Aquí es donde entra yield. Es una palabra clave que usas dentro de una función, de forma muy parecida a return. La gran diferencia es que una función que contiene yield no devuelve un valor y termina, sino que devuelve un objeto generador.
Cuando llamas a una función con yield, el código dentro de la función no se ejecuta de inmediato. Solo se crea y devuelve el objeto generador. Es un poco contraintuitivo al principio, lo sé, a mí esto me costó entenderlo bien.
El código de la función se empieza a ejecutar recién cuando alguien (por ejemplo, un bucle for) pide el siguiente valor al generador. La función corre hasta que encuentra una sentencia yield, devuelve ese valor, y luego pausa su ejecución, manteniendo todo su estado interno.
Cuando se pide el siguiente valor, la función retoma su ejecución exactamente desde donde la dejó, hasta que encuentra el próximo yield o termina. Si la función termina sin encontrar otro yield, el generador se considera agotado.
def mi_generador_simple():
yield 1
yield 2
yield 3
# Cuando llamamos a la funcion, no se ejecuta nada de inmediato
# Solo obtenemos el objeto generador
g = mi_generador_simple()
# Recien ahora empieza la magia
for valor in g:
print(valor)
# Salida:
# 1
# 2
# 3
Este patrón es súper útil cuando sabes que una función va a producir una enorme cantidad de valores, pero solo necesitas procesarlos uno a uno y no todos a la vez. Piensa en la lectura de archivos grandes, el procesamiento de streams de datos, o incluso la creación de secuencias infinitas.
El atajo mental (y por qué no es toda la verdad)
Algunos explican yield con un "atajo" mental: imagínate que cada yield expr es un lista.append(expr) y al final la función hace un return lista. Esto te puede dar una idea de qué valores producirá la función, pero no cómo lo hace. Es una simplificación peligrosa.
De hecho, este "atajo" falla estrepitosamente en dos escenarios críticos:
- Eficiencia de memoria: Si la lista final es enorme, volvemos al problema inicial de agotar la RAM. El propósito de
yieldes precisamente evitar eso. - Secuencias infinitas: Si tu generador está diseñado para producir una secuencia interminable (por ejemplo, números primos), el "atajo" intentaría construir una lista infinita, lo que nunca terminaría o colapsaría.
Así que, si bien puede servir como un primer acercamiento para visualizar los valores, no te quedes solo con esa idea. La clave es entender la pausa y reanudación de la ejecución, y la eficiencia de memoria.
Mi recomendación
En mi experiencia, usar yield es una de esas cosas que, una vez que entiendes, te abren un mundo de posibilidades para escribir código más robusto y eficiente. Yo lo uso principalmente cuando:
- Estoy procesando grandes volúmenes de datos donde cargar todo en memoria es un problema.
- Necesito una secuencia de valores, pero no todos al mismo tiempo, sino uno a uno.
- Quiero implementar un iterable personalizado de forma simple, sin tener que crear una clase completa con métodos
__iter__y__next__(de hecho,yieldlo hace por ti).
Si te encuentras en alguna de estas situaciones, no lo dudes. Usa yield. Tu RAM y el rendimiento de tus aplicaciones te lo agradecerán.