yield
è una parola utilizzata all'interno di una funzione per creare una funzione generatrice, una funzione speciale che può essere interrotta e poi ripresa dall'ultimo punto in cui è stata interrotta.
Questo la rende molto utile per fare iteratori efficienti perché evitano di immagazzinare grandi quantità di dati in memoria.
Quando una funzione ha yield, non restituisce direttamente un valore come una funzione normale, restituisce un oggetto generatore. L'oggetto generatore può essere usato per iterare attraverso i risultati in modo lazy, cioè calcolandoli solo quando necessario.
Facciamo un esempio, immagina questa semplice funzione che calcola una lista di temperature da gradi Celsius a gradi Fahrenheit:
def converti_temperatura(celsius: float) -> list[float]:
return [celsius * 9/5 + 32 for celsius in range(0, 101)]
converti_temperatura(0)
[32.0, 33.8, 35.6, 37.4, 39.2, 41.0, 42.8, 44.6, 46.4, 48.2, 50.0, 51.8, 53.6, 55.4, 57.2, 59.0, 60.8, 62.6, 64.4, 66.2, 68.0, 69.8, 71.6, 73.4, 75.2, 77.0, 78.8, 80.6, 82.4, 84.2, 86.0, 87.8, 89.6, 91.4, 93.2, 95.0, 96.8, 98.6, 100.4, 102.2, 104.0, 105.8, 107.6, 109.4, 111.2, 113.0, 114.8, 116.6, 118.4, 120.2, 122.0, 123.8, 125.6, 127.4, 129.2, 131.0, 132.8, 134.6, 136.4, 138.2, 140.0, 141.8, 143.6, 145.4, 147.2, 149.0, 150.8, 152.6, 154.4, 156.2, 158.0, 159.8, 161.6, 163.4, 165.2, 167.0, 168.8, 170.6, 172.4, 174.2, 176.0, 177.8, 179.6, 181.4, 183.2, 185.0, 186.8, 188.6, 190.4, 192.2, 194.0, 195.8, 197.6, 199.4, 201.2, 203.0, 204.8, 206.6, 208.4, 210.2, 212.0, 213.8, 215.6, 217.4, 219.2, 221.0, 222.8, 224.6, 226.4, 228.2, 230.0, 231.8, 233.6, 235.4, 237.2, 239.0, 240.8]
sum(converti_temperatura(0))
12012.0
Questa funzione calcola la conversione di temperatura per ogni grado Celsius nell'intervallo da 0 a 100 e restituisce una lista e poi si somma.
In questo caso va bene ma se il numero di temperature da convertire cresce notevolmente ad esempio a milioni, il ritardo tra la chiamata della funzione e il ritorno del risultato diventa importante. Oltre quindi al tempo di esecuzione c'è una occupazione di memoria considerevole. Ad esempio una lista di 101 temperature richiede:
import sys
sys.getsizeof([celsius for celsius in range(0, 101)])
1008
Significa che una lista di 101 temperature richiede 1008 byte di memoria, immaginate per milioni di temperature!
Ecco che entra in gioco yield
che viene gestita in modo diverso rispetto alle funzioni standard. Vediamo quindi l'esempio di prima riscritto in modo ottimizzato usando la funzione yield
:
from typing import Generator
def converti_temperatura_generatore() -> Generator[float, None, None]:
for celsius in range(0, 101):
yield celsius * 9/5 + 32
converti_temperatura_generatore()
<generator object converti_temperatura_generatore at 0x10496e960>
sum(converti_temperatura_generatore())
12012.0
La funzione viene chiamata in modo simile a quella di prima ma la tipizzazione è un po' più complessa dato che ci sono tre argomenti: "yield", "send" e "ritorno".
Nel "ritorno" la funzione converti_temperatura_generatore
non ha un'istruzione quindi è coerente con il codice "None", idem per "send".
Quando chiamiamo la funzione converti_temperatura_generatore
inizia a iterare nell'intervallo da 0 a 100 e l'espressione "yield" restituisce immediatamente il valore convertito all'utente. Questo ha due vantaggi: l'utente ha il valore più rapidamente e non è necessario allocare memoria aggiuntiva per memorizzare i valori convertiti.
N.B. le funzioni generatrici sono utilizzate in modo diverso rispetto ad altri iteratori. Quando chiamiamo la funzione restituisce un oggetto "generatore":
converti_temperatura_generatore()
<generator object converti_temperatura_generatore at 0x10496ec00>