Un generator è una funzione o un'espressione che restituisce un tipo speciale di iteratore, chiamato iteratore di generatori.
Generator-iterators sono pigri: non memorizzano i loro values in memoria, ma generano i loro valori quando servono.
Una funzione generatore è simile a qualsiasi altra funzione, ma contiene una o più espressioni yield.
Ogni yield sospende l'esecuzione del codice, salvando lo stato di esecuzione corrente (comprese tutte le variabili locali e le istruzioni try).
Quando il generatore riprende, recupera lo stato dal punto in cui era stato sospeso, a differenza delle funzioni normali, che si azzerano a ogni chiamata.
I generatori si costruiscono più o meno come le altre funzioni iterative o ricorsive, ma richiedono un'espressione yield, che esploreremo in dettaglio tra poco.
Un esempio è una funzione che restituisce i quadrati a partire da un array di numeri.
Così com'è scritta, deve elaborare tutto l'input prima di poter restituire qualsiasi valore:
>>> def squares(list_of_numbers):
... squares = []
... for number in list_of_numbers:
... squares.append(number ** 2)
... return squares
Puoi convertire quella funzione in un generatore così:
>>> def squares_generator(list_of_numbers):
... for number in list_of_numbers:
... yield number ** 2
Il motivo è che usi un generatore quando non ti servono tutti i valori in una volta sola.
Così risparmi memoria e potenza di calcolo, perché viene calcolato solo il valore su cui stai lavorando in quel momento.
I generatori (tecnicamente generator-iterator, vedi la nota qui sotto) sono un tipo di iterator e si possono usare ovunque in Python ci si aspetti un iterator o un iterable.
Questo vale anche per funzioni o oggetti che richiedono un iterable/iterator come argomento.
Per approfondire, vedi How to Make an Iterator in Python.
Gli iteratori di generatori sono un sottoinsieme speciale degli iteratori.
Gli Iterators sono il meccanismo o protocollo che consente di iterare sugli iterabili.
Gli iteratori di generatori e gli iteratori restituiti dai comuni iterables di Python si comportano in modo molto simile, ma ci sono alcune differenze importanti da notare:
indexes.
Non puoi fare riferimento a un valore precedente o futuro usando somme o sottrazioni e non puoi usare la notazione con parentesi quadre ([]) né lo slicing.len(), perché non hanno una lunghezza.generator-iterator infinito!Per usare il generatore squares_generator():
>>> squared_numbers = squares_generator([1, 2, 3, 4])
>>> for square in squared_numbers:
... print(square)
...
1
4
9
16
I valori all'interno di un generatore si possono anche produrre o recuperare tramite la funzione next().
next() chiama il metodo __next__() di un oggetto generatore, «avanzando» o valutando il codice del generatore fino alla sua espressione yield, che a quel punto «produce» o restituisce il valore.
>>> squared_numbers = squares_generator([1, 2])
>>> next(squared_numbers)
1
>>> next(squared_numbers)
4
Quando un generator è completamente consumato e non ha più valori da restituire, solleva un errore StopIteration.
>>> next(squared_numbers)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
I generatori sono un sottoinsieme speciale degli iteratori.
Gli Iterators sono il meccanismo o protocollo che consente di iterare sugli iterabili.
I generatori e gli iteratori restituiti dai comuni iterables di Python si comportano in modo molto simile, ma ci sono alcune differenze importanti da notare:
I generatori sono a senso unico; non si può «tornare indietro» a un valore precedente.
Iterare sui generatori consuma i valori restituiti; nessun ripristino.
I generatori (poiché sono valutati in modo pigro) non si possono ordinare né invertire.
I generatori non hanno indexes, quindi non puoi fare riferimento a un valore precedente o futuro usando somme o sottrazioni.
I generatori non si possono usare con la funzione len().
I generatori possono essere finiti o infiniti: fai attenzione quando raccogli tutti i valori da un generatore infinito.
L'espressione yield è molto simile all'espressione return.
A differenza dell'espressione return, yield cede i valori al chiamante in un punto specifico, sospendendo la valutazione o la restituzione di ulteriori valori finché non vengono richiesti.
Quando viene valutato yield, l'esecuzione della funzione che lo contiene si mette in pausa e vengono restituiti i valori della funzione in quel preciso momento.
La funzione poi resta nello scope, e quando viene chiamato __next__(), l'esecuzione riprende finché non si incontra di nuovo yield.
Usare espressioni yield è vietato al di fuori delle funzioni.
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
>>> lets_try = infinite_sequence()
>>> lets_try.__next__()
0
>>> lets_try.__next__()
1
I generatori sono utili in moltissime applicazioni.
Quando lavori con una raccolta di grandi dimensioni, potresti non voler mettere tutti i suoi valori in memory.
Un generatore può servire a elaborare dati più grandi un pezzo alla volta, risparmiando memoria e migliorando le prestazioni.
I generatori sono anche molto utili quando un processo o un calcolo è complesso, costoso o infinito:
>>> def infinite_sequence():
... current_number = 0
... while True:
... yield current_number
... current_number += 1
Ora, ogni volta che si chiama __next__() sull'oggetto infinite_sequence, verrà restituito il numero precedente + 1.