Last chance! 50% off DataCamp Premium
Sale ends in
Le funzioni Python non hanno sempre un'istruzione return. Le funzioni generatrici sono funzioni che hanno la parola chiave yield invece di return.
Queste funzioni producono iteratori generatori, cioè oggetti che rappresentano un flusso di dati. Gli elementi rappresentati da un iteratore vengono creati e restituiti solo quando richiesti. Questo tipo di valutazione è spesso chiamato valutazione pigra (lazy evaluation).
Quando si lavora con dataset di grandi dimensioni, i generatori offrono un'alternativa efficiente in memoria rispetto all'archiviazione dei dati in liste, tuple e altre strutture dati che richiedono spazio in memoria per ciascun elemento. Le funzioni generatrici possono anche creare iteratori infiniti, cosa non possibile con strutture valutate avidamente come liste e tuple.
Prima di iniziare, ricapitoliamo le differenze tra funzioni e generatori:
|
Caratteristica |
Funzione |
Generatore |
|
Produzione dei valori |
Restituisce tutti i valori in una volta |
Restituisce i valori uno alla volta, su richiesta |
|
Esecuzione |
Esegue completamente prima di restituire |
Si mette in pausa dopo ogni yield, riprende quando è richiesto il valore successivo |
|
Parola chiave |
return |
yield |
|
Uso della memoria |
Potenzialmente elevato, memorizza l'intera sequenza in memoria |
Basso, memorizza solo il valore corrente e lo stato per il successivo |
|
Iterazione |
Possibili iterazioni multiple, ma richiede di memorizzare l'intera sequenza |
Progettato per un'unica passata, più efficiente per sequenze grandi o infinite |
yield di Python per creare funzioni generatriciIl termine generatore in Python può riferirsi a un iteratore generatore o a una funzione generatrice. Sono oggetti diversi ma correlati in Python. In questo tutorial, per evitare confusione, si usano spesso i termini per esteso.
Partiamo dalle funzioni generatrici. Una funzione generatrice assomiglia a una normale funzione ma contiene la parola chiave yield invece di return.
Quando un programma Python chiama una funzione generatrice, crea un iteratore generatore. Gli iteratori restituiscono un valore su richiesta e mettono in pausa l'esecuzione finché non serve un altro valore. Vediamo un esempio per spiegare questo concetto e mostrare la differenza tra funzioni normali e funzioni generatrici.
Per prima cosa, definiamo una funzione normale, che contiene un'istruzione return. Questa funzione accetta una sequenza di parole e una lettera, e restituisce una lista con il numero di occorrenze della lettera in ciascuna parola:
def find_letter_occurrences(words, letter):
output = []
for word in words:
output.append(word.count(letter))
return output
print(
find_letter_occurrences(["apple", "banana", "cherry"], "a")
)
[1, 3, 0]
La funzione restituisce una lista contenente 1, 3 e 0, dato che c'è una a in apple, tre occorrenze di a in banana e nessuna in cherry. La stessa funzione può essere riscritta usando le list comprehension invece di inizializzare una lista vuota e usare .append():
def find_letter_occurrences(words, letter):
return [word.count(letter) for word in words]
Questa funzione normale restituisce una lista con tutti i risultati ogni volta che viene chiamata. Tuttavia, se la lista di parole è grande, chiamare questa funzione normale richiede molta memoria, poiché il programma crea e memorizza una nuova lista della stessa dimensione dell'originale. Se questa funzione viene usata ripetutamente su diversi argomenti di input, o funzioni simili eseguono altre operazioni sui dati originali, la pressione sulla memoria può aumentare rapidamente.
Al suo posto si può usare una funzione generatrice:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print(output)
<generator object find_letter_occurrences at 0x102935e00>
La funzione include la parola chiave yield invece di return. Questa funzione generatrice restituisce un oggetto generatore quando viene chiamata, assegnato a output. Questo oggetto è un iteratore. Non contiene i dati con il numero di occorrenze della lettera in ciascuna parola. Il generatore, invece, creerà e produrrà i valori quando servono. Recuperiamo il primo valore da questo iteratore generatore:
print(next(output))
1
La funzione built-in next() è un modo per ottenere il valore successivo da un iteratore. Più avanti in questo tutorial vedremo altri modi.
Il codice nella funzione generatrice viene eseguito fino a quando il programma raggiunge la riga con la parola chiave yield. In questo esempio, il ciclo for inizia la prima iterazione e preleva il primo elemento nella lista words. Il metodo di stringa .count() restituisce un intero, che in questo caso è 1, poiché c'è un'occorrenza di a in apple. Il generatore produce questo valore, che viene restituito da next(output).
Il generatore output mette in pausa l'esecuzione a questo punto. Quindi il generatore ha completato la prima iterazione del ciclo for e ha trovato il numero di occorrenze della lettera a nella prima parola dell'elenco. Ora attende finché non serve di nuovo.
Se la funzione built-in next() viene chiamata di nuovo con output come argomento, il generatore riprenderà l'esecuzione dal punto in cui era in pausa:
print(next(output))
3
Il generatore riparte dalla riga con yield nella prima iterazione del ciclo for. Poiché il ciclo for non ha altre righe di codice, torna all'inizio del ciclo e preleva il secondo elemento dalla lista words. Il valore restituito da .count() è 3 in questo caso, e questo valore viene prodotto. Il generatore si mette di nuovo in pausa a questo punto dell'esecuzione.
La terza chiamata a next() riprende questa esecuzione:
print(next(output))
0
La seconda iterazione raggiunge la fine del ciclo for, che passa alla terza iterazione. Il codice avanza di nuovo fino alla riga con yield, questa volta producendo l'intero 0 poiché non ci sono occorrenze di a in cherry.
Il generatore si mette di nuovo in pausa. Il programma determina il destino del generatore solo quando chiamiamo next() una quarta volta:
print(next(output))
Traceback (most recent call last):
...
StopIteration
L'esecuzione riprende dalla fine della terza iterazione del ciclo for. Tuttavia, il ciclo ha raggiunto la fine poiché non ci sono più elementi nella lista words. Il generatore solleva un'eccezione StopIteration.
Nella maggior parte dei casi d'uso, gli elementi del generatore non vengono accessi direttamente con next() ma tramite un altro processo di iterazione. L'eccezione StopIteration segnala la fine del processo di iterazione. Approfondiremo questo aspetto nella sezione seguente del tutorial.
Python offre un altro modo per creare iteratori generatori quando la loro operazione può essere rappresentata da una singola espressione, come nell'esempio precedente. L'iteratore generatore output può essere creato usando un espressione generatrice:
words = ["apple", "banana", "cherry"]
letter = "a"
output = (word.count(letter) for word in words)
print(next(output))
print(next(output))
print(next(output))
print(next(output))
1
3
0
Traceback (most recent call last):
...
StopIteration
L'espressione tra parentesi assegnata a output è un'espressione generatrice, che crea un iteratore generatore simile a quello prodotto dalla funzione generatrice find_letter_occurrences().
Concludiamo questa sezione con un altro esempio di funzione generatrice per evidenziare come l'esecuzione si metta in pausa e riprenda ogni volta che serve un elemento:
def show_status():
print("Start")
yield
print("Middle")
yield
print("End")
yield
status = show_status()
next(status)
Start
Questa funzione generatrice non ha un ciclo. Contiene invece tre righe con la parola chiave yield. Il codice crea un iteratore generatore status quando chiama la funzione generatrice show_status(). La prima volta che il programma chiama next(status), il generatore avvia l'esecuzione. Stampa la stringa "Start" e si mette in pausa dopo la prima espressione yield. Il generatore produce None poiché non c'è alcun oggetto dopo la parola chiave yield.
La stringa "Middle" viene stampata solo quando next() viene chiamato una seconda volta:
next(status)
Middle
Il generatore si mette in pausa dopo la seconda espressione yield. La terza chiamata a next() stampa la stringa finale, "End":
next(status)
End
Il generatore si mette in pausa sull'ultima espressione yield. Solleverà un'eccezione StopIteration alla successiva richiesta di un valore da questo iteratore generatore:
next(status)
Traceback (most recent call last):
...
StopIteration
Esploreremo altri modi di usare i generatori nella sezione seguente.
Le funzioni generatrici creano iteratori generatori, e gli iteratori sono iterabili. Scomponiamo questa frase. Ogni volta che il programma chiama una funzione generatrice, crea un iteratore. Poiché gli iteratori sono iterabili, possono essere usati nei cicli for e in altri processi iterativi.
Quindi, la funzione built-in next() non è l'unico modo per accedere agli elementi di un iteratore. In questa sezione esploriamo altri modi di lavorare con i generatori.
Riprendiamo una funzione generatrice vista in una sezione precedente di questo tutorial:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
for value in output:
print(value)
1
3
0
Invece di usare next() più volte, questa versione del codice usa l'iteratore generatore output in un ciclo for. Poiché gli iteratori sono iterabili, possono essere usati nei cicli for. Il ciclo preleva elementi dall'iteratore generatore finché non ci sono più valori.
A differenza di strutture dati come liste e tuple, un iteratore può essere usato una sola volta. Il codice non ristampa i valori se proviamo a eseguire lo stesso ciclo for una seconda volta:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print("First attempt:")
for value in output:
print(value)
print("Second attempt:")
for value in output:
print(value)
First attempt:
1
3
0
Second attempt:
L'iteratore viene esaurito dal primo ciclo for, quindi non può più produrre valori. Se serve di nuovo il generatore dopo che è esaurito, dobbiamo creare un altro iteratore generatore dalla funzione generatrice.
È anche possibile avere più iteratori generatori contemporaneamente in un programma:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
first_output = find_letter_occurrences(words, letter)
second_output = find_letter_occurrences(words, letter)
print("First value of first_output:")
print(next(first_output))
print("Values of second_output:")
for value in second_output:
print(value)
print("Remaining values of first_output:")
for value in first_output:
print(value)
First value of first_output:
1
Values of second_output:
1
3
0
Remaining values of first_output:
3
0
La funzione generatrice find_letter_occurrences() crea due iteratori generatori: first_output e second_output. Sebbene entrambi si riferiscano agli stessi dati nella lista words, avanzano in modo indipendente l'uno dall'altro.
In questo esempio si preleva il primo valore da first_output usando next(). L'iteratore generatore produce 1 e si mette in pausa. Il programma cicla poi su second_output. Poiché questo generatore non ha ancora prodotto alcun valore, il ciclo attraversa tutti i valori prodotti dal secondo iteratore. Infine, c'è un altro ciclo for che itera su first_output. Tuttavia, questo iteratore ha già prodotto il suo primo valore in precedenza. Il ciclo attraversa i valori rimanenti in questo iteratore.
Il ciclo for non è l'unico processo che può essere usato per iterare su iteratori generatori:
print(*find_letter_occurrences(words, letter))
print(sorted(find_letter_occurrences(words, letter)))
1 3 0
[0, 1, 3]
In questi esempi, il programma chiama direttamente la funzione generatrice per creare e usare l'iteratore generatore invece di assegnarlo a una variabile. Nel primo esempio, l'iteratore viene spacchettato usando l'asterisco. Questo processo si basa sullo stesso protocollo di iterazione del ciclo for.
Nel secondo esempio, l'iteratore generatore viene passato alla funzione built-in sorted(), che richiede un argomento iterabile. I generatori sono iterabili e, quindi, possono essere usati ogni volta che avviene un'iterazione in Python.
Un generatore produce un valore e si mette in pausa finché non è necessario il successivo. Ogni volta che il codice richiede un valore da un iteratore, il codice nella funzione generatrice verrà eseguito fino alla valutazione della successiva espressione yield. In tutti gli esempi visti finora, la funzione generatrice aveva un numero finito di espressioni yield. Tuttavia, è possibile creare un generatore che produca un numero infinito di valori usando un ciclo while nella funzione generatrice. Nel seguente esempio, il generatore restituisce un colore casuale dall'elenco di colori passato alla funzione generatrice:
import random
def get_color(colors):
while True:
yield random.choice(colors)
output_colors = get_color(["red", "green", "blue"])
print("First two colors:")
print(next(output_colors))
print(next(output_colors))
print("Next 10 colors using a 'for' loop:")
for _ in range(10):
print(next(output_colors))
First two colors:
green
red
Next 10 colors using a 'for' loop:
blue
green
green
green
red
red
red
blue
green
red
La funzione generatrice get_color() ha un'espressione yield all'interno di un ciclo while. Pertanto, il codice incontrerà sempre un'altra espressione yield mentre cerca il valore successivo. L'iteratore generatore output_colors produce un numero infinito di colori scelti casualmente dalla lista di input. Questo generatore non si esaurirà mai.
Non è possibile creare strutture dati infinite come liste e tuple. I generatori permettono a un programma di creare iterabili infiniti. Nota che se l'iteratore generatore viene usato direttamente in un ciclo for, il ciclo andrà avanti per sempre.
I generatori hanno casi d'uso più avanzati in Python. In questa sezione ne esploreremo alcuni.
I generatori possono anche accettare dati aggiuntivi da usare durante la valutazione del codice. L'istruzione che contiene la parola chiave yield è un'espressione che viene valutata a un valore. Questo valore può essere assegnato a una variabile all'interno della funzione generatrice. Iniziamo con un esempio di base per illustrare questo concetto:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(next(output))
print(next(output))
1
The yield expression evaluates to: None
2
The yield expression evaluates to: None
Traceback (most recent call last):
...
StopIteration
La parola chiave yield in Python crea un'espressione che viene valutata a un valore. Tuttavia, il valore di questa espressione all'interno della funzione generatrice non è lo stesso oggetto prodotto dal generatore. Considera la prima espressione yield. Il generatore produce l'intero 1. Quindi print(next(output)) visualizza 1 alla prima chiamata e mette in pausa l'esecuzione del generatore.
Tuttavia, l'espressione yield nella funzione generatrice si valuta in un oggetto, che il codice assegna al nome di variabile value. In questo esempio, yield assegna None a value. Questo processo si ripete per la seconda occorrenza di yield nella funzione generatrice. Lo scopo della terza chiamata a next() è assicurarsi che tutto il codice nella funzione generatrice venga eseguito.
Sostituiamo la seconda e la terza chiamata a next() con .send(), che è un metodo della classe generator:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(output.send("Here's a value"))
print(output.send("Here's another value"))
1
The yield expression evaluates to: Here's a value
2
The yield expression evaluates to: Here's another value
Traceback (most recent call last):
...
StopIteration
La funzione generatrice è invariata. Il generatore viene avviato chiamando next() e il codice gira finché non produce il primo intero, 1. Invece di usare next() la seconda volta, il programma chiama output.send(). Questo metodo invia un oggetto al generatore. In questo esempio, l'oggetto è una stringa. L'espressione yield nella funzione generatrice si valuta in questa stringa, che viene assegnata a value. Pertanto, il generatore può usare la stringa nel suo codice.
La seconda chiamata a .send() invia un nuovo oggetto al generatore, assegnato alla stessa variabile value. Dopo l'ultima chiamata a print() il generatore solleva StopIteration poiché non ci sono più espressioni yield.
Vediamo un altro esempio con .send(). Il seguente generatore visualizza il saldo di un conto, ma il saldo può essere aggiornato:
def get_balance(start_balance):
balance = start_balance
while True:
amount = yield balance
if amount is not None:
balance += amount
current_balance = get_balance(100)
print(next(current_balance))
print(current_balance.send(10))
print(current_balance.send(-20))
print(next(current_balance))
100
110
90
90
La funzione generatrice richiede un saldo iniziale quando viene chiamata. Il valore di balance può cambiare mentre il generatore esegue il codice. Qualsiasi oggetto inviato al generatore con .send() viene assegnato a amount. Questa variabile sarà None se il generatore produce un valore senza che gli venga inviato alcun oggetto, oppure conterrà l'oggetto inviato tramite .send().
L'iteratore generatore current_balance parte con un saldo di 100 $. Il generatore viene avviato chiamando next(), che fa partire l'esecuzione del codice fino alla produzione del primo valore.
Una volta avviato il generatore, è possibile far ripartire l'esecuzione usando .send() invece di next(). Il generatore aggiunge il valore inviato al saldo. Se non viene inviato alcun valore, come quando si chiama di nuovo next(), il generatore restituisce il saldo invariato.
I generatori Python possono anche produrre valori direttamente da un altro generatore o iterabile usando la sintassi yield from. Vediamo un esempio di funzione generatrice che produce valori da una lista annidata:
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
nested_list = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(nested_list)))
[1, 2, 3, 4, 5, 6, 7]
La funzione generatrice accetta una lista, che può includere al suo interno liste annidate. Il ciclo for itera sugli elementi della lista. Ogni elemento della lista esterna è o un valore, in questo caso un intero, o un'altra lista. Quando l'elemento non è una lista, il generatore lo produce.
Tuttavia, quando l'elemento è una lista, il generatore richiama ricorsivamente la funzione generatrice flatten() con la lista interna come argomento. Questo crea un altro iteratore generatore, che usa la lista interna come sorgente dati. Se questa riga usasse un'espressione yield, il primo generatore produrrebbe il secondo generatore. Invece, usando yield from, il primo generatore produce i valori del secondo generatore.
yield vs. returnLe definizioni di funzione con return e yield sembrano simili, ma il loro comportamento è diverso. Riassumiamo le differenze chiave:
|
Funzione normale |
Funzione generatrice |
|
|
Parola chiave |
return (implicita se non usata esplicitamente) |
yield |
|
Chiamata |
Esegue il codice fino a incontrare return, poi restituisce il valore finale |
Crea un iteratore generatore |
|
Terminazione |
Terminata dall'istruzione return |
Messa in pausa da yield, può essere ripresa più tardi |
|
Valore restituito |
Singolo oggetto (può essere una struttura dati) |
Iteratore generatore |
|
Espressione yield |
Non applicabile (crea un'istruzione) |
Valuta a None o al valore inviato con .send() |
|
Casi d'uso |
Ideale per restituire un risultato finale |
Ideale per creare un flusso di dati, specialmente sequenze grandi o infinite |
La parola chiave yield di Python è usata nelle funzioni per definire una funzione generatrice. Quando chiamate, queste funzioni creano iteratori generatori. I generatori sono un esempio di valutazione pigra in Python, in cui le espressioni vengono valutate quando serve il valore invece che al momento dell'esecuzione dell'espressione. Pertanto, l'espressione yield è utile per creare un flusso di dati in cui i valori vengono generati su richiesta senza bisogno di memorizzarli in memoria.
Le considerazioni sull'efficienza sono importanti quando si lavora con grandi dataset che richiedono molte operazioni. Gli iteratori generatori di Python sono uno degli strumenti principali per manipolare grandi quantità di dati in modo efficiente.
Se vuoi saperne di più su Python, dai un'occhiata a questo percorso di carriera Python Developer.
Ho studiato Fisica e Matematica a livello UG presso l’Università di Malta. Poi mi sono trasferito a Londra e ho conseguito il dottorato in Fisica all’Imperial College. Ho lavorato su nuove tecniche ottiche per immagini della retina umana. Ora mi concentro sullo scrivere di Python, comunicare su Python e insegnare Python.
Impara Python con questi corsi!
{count, plural, one {Corso} other {Corsi}}
{count, plural, one {Corso} other {Corsi}}
{count, plural, one {Programma} other {Programmi}}

{count, plural, one {blog} other {blogs}}
Abid Ali Awan
10 min
{count, plural, one {blog} other {blogs}}
Tim Lu
12 min

{count, plural, one {blog} other {blogs}}
Abid Ali Awan
15 min