Nota Build
yield en Python, un next() a la vez
Seguí un ejemplo de tres números para entender cómo yield pausa un generador de Python, cómo next() lo reanuda y cómo termina la iteración.
yield permite que una función entregue un valor a quien la llamó y se pause. Cuando se solicita otro valor, la ejecución continúa desde donde quedó.
Una función definida con def que contiene yield es una función generadora. Llamarla crea un objeto generador, que produce valores a medida que se los pedís.
Empecemos por la salida
Ejecutá este ejemplo con Python 3:
def generate_numbers():
print("Preparing 10")
yield 10
print("Preparing 20")
yield 20
print("Preparing 30")
yield 30
numbers = generate_numbers()
print("Generator created")
print("Received:", next(numbers))
print("Received:", next(numbers))
print("Received:", next(numbers))
Imprime:
Generator created
Preparing 10
Received: 10
Preparing 20
Received: 20
Preparing 30
Received: 30
Observá que Generator created aparece primero. Cuando generate_numbers() devuelve el generador, todavía no se ejecutó ninguna instrucción del cuerpo de la función.
Un pedido a la vez
Seguí el mismo objeto numbers a lo largo del programa:
- Creación:
numbers = generate_numbers()crea el generador. Todavía no aparece ningún mensajePreparing. - Primer
next(numbers): la ejecución empieza al principio de la función. ImprimePreparing 10, llega ayield 10y se pausa. La llamada anext()devuelve10, así que elprint()externo imprimeReceived: 10. - Segundo
next(numbers): la ejecución continúa después deyield 10. ImprimePreparing 20, entrega20y vuelve a pausarse. - Tercer
next(numbers): la ejecución continúa después deyield 20. ImprimePreparing 30, entrega30y se pausa ahí.
Python evalúa next(numbers) antes de que el print() que lo contiene pueda imprimir sus argumentos. Por eso cada línea Preparing aparece antes de su correspondiente línea Received.
El generador recuerda su posición y sus variables locales entre pedidos. Retoma la misma ejecución en lugar de empezar la función de nuevo. Estas son las reglas de suspensión descritas en la referencia de yield de Python.
¿En qué se diferencia yield de return?
return termina la ejecución actual de la función. En una función común, return 10 entrega 10 a quien la llamó y finaliza esa llamada.
yield 10 entrega un valor a quien llamó a next() y conserva la ejecución del generador para poder continuar más adelante. Un return dentro de un generador lo termina; no entrega otro elemento mediante yield.
La pausa no inicia una tarea en segundo plano. En este ejemplo, no se prepara nada más hasta que pedís otro valor.
¿Qué pasa con el cuarto next()?
Después de la tercera llamada, el generador está pausado en yield 30. Agregá esta línea al ejemplo original:
next(numbers)
La ejecución se reanuda, llega al final de la función y lanza StopIteration. Esa excepción indica que no quedan elementos. Las siguientes llamadas sobre el generador agotado también lanzan StopIteration; no lo reinician.
Si querés recibir un valor alternativo en lugar de esa excepción, usá el segundo argumento opcional de next():
print(next(numbers, "Done"))
Ubicada inmediatamente después del ejemplo original, esta línea imprime Done.
Dejá que un for pida los valores
Normalmente, un bucle for se encarga de los pedidos y se detiene automáticamente cuando se agota el generador. Conservá la definición de la función y reemplazá el código que viene después por:
for number in generate_numbers():
print("Received:", number)
Esto crea un generador nuevo e imprime las mismas líneas alternadas de Preparing y Received, sin Generator created. Volver a llamar a generate_numbers() te da una secuencia nueva para consumir.
Los generadores sirven cuando querés procesar valores a medida que están disponibles sin reunir primero todos los resultados en una lista. Eso puede ahorrar memoria con secuencias grandes, aunque el generador y quien lo consume siguen usando memoria. Los tres números de este ejemplo hacen visible el momento de ejecución: cada pedido avanza solo hasta el próximo valor.