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>

Per ogni caso ho anche aggiunto la libreria clock per vedere la differenza di tempo impiegata nell'operazione:

Primo caso:

import time

def converti_temperatura(celsius: float) -> list[float]:
    start_time = time.time()
    result = [celsius * 9/5 + 32 for celsius in range(0, 10_000_000)]
    end_time = time.time()
    elapsed_time = end_time - start_time
    return result, elapsed_time

temperatures, elapsed_time_list = converti_temperatura(0)

print(f"Conversione di temperatura per la lista impiega {elapsed_time_list:.4f} secondi.")
print(f"Numero di temperature convertite: {len(temperatures)}")

Risultato:

Conversione di temperatura per la lista impiega 4.5467 secondi.
Numero di temperature convertite: 10,000,000

Secondo caso:

import time

from typing import Generator

def converti_temperatura_generatore() -> Generator[float, None, None]:
    start_time = time.time()
    temperatures = [celsius * 9/5 + 32 for celsius in range(0, 10_000_000)]
    for temperature in temperatures:
        yield temperature
    end_time = time.time()
    elapsed_time = end_time - start_time
    yield elapsed_time

generator = converti_temperatura_generatore()
for value in generator:
    if isinstance(value, float):
        continue
    elapsed_time_generator = value

print(f"Conversione di temperatura con un generatore impiega {elapsed_time_generator:.4f} secondi.")
print(f"Numero di temperature convertite: 10,000,000")

Risultato:

Conversione di temperatura con un generatore impiega 0.5891 secondi.
Numero di temperature convertite: 10,000,000

Direi una bella differenza! Soprattutto con linguaggi di programmazione come Python!

Powered by: FreeFlarum.
(remove this footer)