Corso
Gli iteratori sono oggetti che possono essere iterati. Sono una caratteristica comune del linguaggio Python, spesso usata in loop e comprensioni di liste. Qualsiasi oggetto dal quale si possa ottenere un iteratore è detto iterable.
Costruire un iteratore richiede parecchio lavoro. Ad esempio, l’implementazione di ogni oggetto iteratore deve includere i metodi __iter__() e __next__() . Oltre a questo prerequisito, l’implementazione deve anche tracciare lo stato interno dell’oggetto e sollevare un’eccezione StopIteration quando non ci sono più valori da restituire. Queste regole sono note come protocollo dell’iteratore.
Implementare un iteratore personalizzato è un processo lungo e non sempre necessario. Un’alternativa più semplice è usare un generatore. I generatori sono un tipo speciale di funzione che usa la keyword yield per restituire un iteratore che può essere iterato, un valore alla volta.
Capire quando conviene implementare un iteratore o usare un generatore migliorerà le tue abilità come programmatore Python. Nel resto di questo tutorial, metteremo in evidenza le differenze tra i due oggetti per aiutarti a scegliere il migliore a seconda dei casi.
Glossario
|
Termine |
Definizione |
|
Iterable |
Un oggetto Python che può essere percorso in un loop. Esempi di iterable includono liste, set, tuple, dizionari, stringhe, ecc. |
|
Iterator |
Un iteratore è un oggetto che può essere iterato. Di conseguenza, gli iteratori contengono un numero contabile di valori. |
|
Generatore |
Un tipo speciale di funzione che non restituisce un singolo valore: restituisce un oggetto iteratore con una sequenza di valori. |
|
Valutazione pigra (Lazy Evaluation) |
Una strategia di valutazione per cui certi oggetti vengono prodotti solo quando richiesti. Di conseguenza, in alcuni ambienti di sviluppo la valutazione pigra è anche chiamata “call-by-need”. |
|
Protocollo dell’iteratore |
Un insieme di regole da seguire per definire un iteratore in Python. |
|
next() |
Una funzione built-in usata per restituire l’elemento successivo in un iteratore. |
|
iter() |
Una funzione built-in usata per convertire un iterable in un iteratore. |
|
yield() |
Una keyword di Python simile a return, tranne che |
Iteratori e iterable in Python
Gli iterable sono oggetti in grado di restituire i propri elementi uno alla volta: possono essere iterati. Le strutture dati Python più diffuse, come liste, tuple e set, sono iterable. Anche altre strutture dati come stringhe e dizionari sono considerate iterable: una stringa può essere iterata sui suoi caratteri e le chiavi di un dizionario possono essere iterate. Come regola generale, considera iterable qualsiasi oggetto che può essere percorso con un for-loop.
Esplorare gli iterable in Python con esempi
Dalle definizioni possiamo concludere che tutti gli iteratori sono anche iterable. Tuttavia, non ogni iterable è necessariamente un iteratore. Un iterable produce un iteratore solo quando viene iterato.
Per dimostrare questa funzionalità, istanzieremo una lista, che è un iterable, e produrremo un iteratore chiamando la funzione built-in iter() sulla lista.
list_instance = [1, 2, 3, 4]
print(iter(list_instance))
"""
<list_iterator object at 0x7fd946309e90>
"""
Sebbene la lista di per sé non sia un iteratore, chiamare la funzione iter() la converte in un iteratore e restituisce l’oggetto iteratore.
Per dimostrare che non tutti gli iterable sono iteratori, istanzieremo la stessa lista e proveremo a chiamare la funzione next(), che serve a restituire l’elemento successivo in un iteratore.
list_instance = [1, 2, 3, 4]
print(next(list_instance))
"""
--------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0cb076ed2d65> in <module>()
3 print(iter(list_instance))
4
----> 5 print(next(list_instance))
TypeError: 'list' object is not an iterator
"""
Nel codice sopra, puoi vedere che il tentativo di chiamare next() sulla lista ha sollevato un TypeError – scopri di più su gestione di eccezioni ed errori in Python. Questo comportamento si verifica perché una lista è un iterable e non un iteratore.
Esplorare gli iteratori in Python con esempi
Quindi, se l’obiettivo è iterare su una lista, bisogna prima ottenere un oggetto iteratore. Solo allora possiamo gestire l’iterazione attraverso i valori della lista.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# convert the list to an iterator
iterator = iter(list_instance)
# return items one at a time
print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))
"""
1
2
3
4
"""
Python produce automaticamente un oggetto iteratore ogni volta che provi a ciclare su un oggetto iterable.
# instantiate a list object
list_instance = [1, 2, 3, 4]
# loop through the list
for item in list_instance:
print(item)
"""
1
2
3
4
"""
Quando viene intercettata l’eccezione StopIteration, il loop termina.
I valori ottenuti da un iteratore possono essere recuperati solo da sinistra a destra. Python non ha una funzione previous() che permetta di tornare indietro in un iteratore.
La natura pigra degli iteratori
È possibile definire più iteratori basati sullo stesso oggetto iterable. Ogni iteratore manterrà il proprio stato di avanzamento. Quindi, definendo più istanze di iteratore di uno stesso iterable, è possibile arrivare alla fine con una istanza mentre un’altra rimane all’inizio.
list_instance = [1, 2, 3, 4]
iterator_a = iter(list_instance)
iterator_b = iter(list_instance)
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"A: {next(iterator_a)}")
print(f"B: {next(iterator_b)}")
"""
A: 1
A: 2
A: 3
A: 4
B: 1
"""
Nota che iterator_b stampa il primo elemento della serie.
Possiamo quindi dire che gli iteratori hanno una natura pigra: quando un iteratore viene creato, gli elementi non vengono prodotti finché non sono richiesti. In altre parole, gli elementi della nostra lista verrebbero restituiti solo quando li chiediamo esplicitamente con next(iter(list_instance)).
Tuttavia, è possibile estrarre tutti i valori da un iteratore in una volta sola chiamando un contenitore di strutture dati iterable built-in (cioè list(), set(), tuple()) sull’oggetto iteratore per forzarlo a generare tutti i suoi elementi contemporaneamente.
# instantiate iterable
list_instance = [1, 2, 3, 4]
# produce an iterator from an iterable
iterator = iter(list_instance)
print(list(iterator))
"""
[1, 2, 3, 4]
"""
Questo non è consigliato per iteratori grandi perché costringe a generare e mantenere in memoria tutti gli elementi in una volta sola, vanificando lo scopo della valutazione pigra.
Quando un dataset è troppo grande per stare comodamente in memoria, o quando vuoi un’iterazione pigra senza scrivere un’intera classe di iteratore, di solito un generatore è più adatto.
Generatori in Python
L’alternativa più rapida all’implementazione di un iteratore è usare un generatore. Anche se i generatori possono sembrare normali funzioni Python, sono diversi. Per cominciare, un generatore non restituisce elementi direttamente. Invece usa la keyword yield per generare elementi al volo. Possiamo quindi dire che un generatore è un tipo speciale di funzione che sfrutta la valutazione pigra.
I generatori non memorizzano il loro contenuto in memoria come ti aspetteresti da un tipico iterable. Per esempio, se l’obiettivo fosse trovare tutti i divisori di un intero positivo, in genere implementeremmo una funzione tradizionale (scopri di più sulle funzioni in Python in questo tutorial) come segue:
def factors(n):
factor_list = []
for val in range(1, n+1):
if n % val == 0:
factor_list.append(val)
return factor_list
print(factors(20))
"""
[1, 2, 4, 5, 10, 20]
"""
Il codice sopra restituisce l’intera lista dei divisori. Nota però la differenza quando si usa un generatore al posto di una funzione tradizionale di Python:
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
print(factors(20))
"""
<generator object factors at 0x7fd938271350>
"""
Dato che abbiamo usato la keyword yield invece di return, l’esecuzione della funzione non termina dopo la chiamata. In sostanza, abbiamo detto a Python di creare un oggetto generatore invece di una funzione tradizionale, consentendo di tracciare lo stato del generatore.
Di conseguenza, è possibile chiamare la funzione next() sull’iteratore pigro per mostrare gli elementi della serie uno alla volta.
def factors(n):
for val in range(1, n+1):
if n % val == 0:
yield val
factors_of_20 = factors(20)
print(next(factors_of_20))
"""
1
"""
Un altro modo per creare un generatore è con una comprehension di generatore. Le espressioni generatore adottano una sintassi simile a quella delle list comprehension, ma usano le parentesi tonde invece delle quadre.
factor_gen = (val for val in range(1, 21) if 20 % val == 0)
print(list(factor_gen))
"""
[1, 2, 4, 5, 10, 20]
"""
Esplorare la keyword yield di Python
La keyword yield controlla il flusso di una funzione generatore. Invece di uscire dalla funzione come fa return, la keyword yield restituisce dalla funzione ma ricorda lo stato delle sue variabili locali.
Il generatore restituito dalla chiamata a yield può essere assegnato a una variabile e percorso con la funzione next() – ciò eseguirà la funzione fino alla prima keyword yield che incontra. Una volta raggiunta yield, l’esecuzione della funzione viene sospesa. Quando questo accade, lo stato della funzione viene salvato. Quindi possiamo riprendere l’esecuzione quando vogliamo.
La funzione continuerà dal punto della chiamata a yield. Ad esempio:
def yield_multiple_statements():
yield "This is the first statement"
yield "This is the second statement"
yield "This is the third statement"
yield "This is the last statement. Don't call next again!"
example = yield_multiple_statements()
print(next(example))
print(next(example))
print(next(example))
print(next(example))
print(next(example))
"""
This is the first statement
This is the second statement
This is the third statement
This is the last statement. Don't call next again or else!
--------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-25-4aaf9c871f91> in <module>()
11 print(next(example))
12 print(next(example))
---> 13 print(next(example))
StopIteration:
"""
Nel codice sopra, il nostro generatore ha quattro chiamate a yield, ma proviamo a chiamare next cinque volte, il che genera un’eccezione StopIteration. Questo comportamento si verifica perché il nostro generatore non è una serie infinita, quindi chiamarlo più volte del previsto esaurisce il generatore.
Riepilogo
Ricapitolando, gli iteratori sono oggetti che possono essere iterati e i generatori sono funzioni speciali che sfruttano la valutazione pigra. Implementare un iteratore personalizzato significa dover creare i metodi __iter__() e __next__(), mentre un generatore può essere implementato usando la keyword yield in una funzione o in una comprehension di Python.
Potresti preferire un iteratore personalizzato rispetto a un generatore quando ti serve un oggetto con una gestione dello stato complessa o se vuoi esporre altri metodi oltre a __next__(), __iter__() e __init__(). D’altra parte, un generatore può essere preferibile quando lavori con grandi insiemi di dati, dato che non memorizza i contenuti in memoria, o quando non è necessario implementare un iteratore.

FAQS
Qual è la differenza tra un iteratore e un generatore in Python?
Un iteratore è qualsiasi oggetto che implementa __iter__() e __next__(). Un generatore è un modo più semplice per creare un iteratore usando una funzione con la keyword yield. Tutti i generatori sono iteratori, ma non tutti gli iteratori sono generatori.
Quando dovrei usare un generatore invece di una lista in Python?
Usa un generatore per sequenze grandi o infinite, o quando l’efficienza in memoria è importante. Le liste mantengono ogni elemento in memoria contemporaneamente, mentre i generatori producono un valore alla volta. Per dataset piccoli che riutilizzerai, una lista di solito va bene.
Cosa fa la keyword yield in Python?
La keyword yield trasforma una funzione in un generatore. Invece di restituire ed uscire, yield mette in pausa la funzione, restituisce un valore e ne ricorda lo stato così l’esecuzione può riprendere alla chiamata successiva.
Come si crea un generatore in Python?
Scrivi una funzione che usi yield invece di return, oppure usa un’espressione generatore — stessa sintassi di una list comprehension ma con parentesi tonde, ad esempio (x * 2 for x in range(10)).
I generatori sono più veloci degli iteratori in Python?
Non in termini di velocità pura, ma sono più efficienti in memoria perché producono valori su richiesta. Per dataset grandi ciò spesso significa prestazioni complessive migliori; per quelli piccoli, la differenza è trascurabile.

