35. Come funziona l'iterazione: iterabili e iteratori
In tutto questo libro, hai usato i cicli (loop) for per iterare su liste, stringhe, dizionari e altre collezioni. Hai scritto codice come for item in my_list: innumerevoli volte. Ma cosa succede davvero dietro le quinte quando Python esegue un ciclo for? Come fa Python a sapere come avanzare attraverso diversi tipi di collezioni?
In questo capitolo esploreremo il protocollo di iterazione (iteration protocol) di Python—il meccanismo che fa funzionare i cicli for. Imparerai cosa sono gli iterabili (iterables) (oggetti su cui puoi eseguire un ciclo) e gli iteratori (iterators) (oggetti che eseguono davvero l’avanzamento tra i valori). Comprendere questa distinzione approfondirà la tua conoscenza di come funziona Python e ti preparerà a lavorare con i generatori nel Capitolo 36.
35.1) Cosa significa che un oggetto è iterabile
35.1.1) Il concetto di iterabilità
Un iterabile (iterable) è qualsiasi oggetto Python su cui si può eseguire un ciclo con un ciclo for. Quando diciamo “eseguire un ciclo”, intendiamo che Python può recuperare gli elementi dell’oggetto uno alla volta, in sequenza.
Hai già lavorato con molti iterabili:
# Le liste sono iterabili
numbers = [1, 2, 3, 4, 5]
for num in numbers:
print(num) # Output: 1, 2, 3, 4, 5 (on separate lines)
# Le stringhe sono iterabili
text = "Python"
for char in text:
print(char) # Output: P, y, t, h, o, n (on separate lines)
# I dizionari sono iterabili (di default sulle chiavi)
student = {"name": "Alice", "age": 20, "grade": "A"}
for key in student:
print(key) # Output: name, age, grade (on separate lines)Tutti questi oggetti—liste, stringhe, dizionari, tuple, set, range e file—sono iterabili perché supportano il protocollo di iterazione (iteration protocol) di Python (un insieme di regole che consente a Python di iterare su di essi).
35.1.2) Cosa rende un oggetto iterabile
Perché un oggetto sia iterabile, deve implementare un metodo speciale chiamato __iter__(). Questo metodo restituisce un oggetto iteratore (iterator). Non preoccuparti ancora dei dettagli: esploreremo gli iteratori nella prossima sezione.
Puoi verificare se un oggetto è iterabile provando a ottenere un iteratore da esso usando la funzione built-in iter():
# Verificare se gli oggetti sono iterabili
numbers = [1, 2, 3]
iterator = iter(numbers) # Funziona - le liste sono iterabili
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hello"
iterator = iter(text) # Funziona - le stringhe sono iterabili
print(type(iterator)) # Output: <class 'str_iterator'>
# Provare con un oggetto non iterabile
value = 42
try:
iterator = iter(value) # Fallisce - gli interi non sono iterabili
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'int' object is not iterableQuando chiami iter() su un oggetto iterabile, Python chiama il metodo __iter__() dell’oggetto e restituisce un iteratore. Se l’oggetto non ha questo metodo, ottieni un TypeError.
35.1.3) Iterabili vs sequenze
È importante capire che non tutti gli iterabili sono sequenze. Una sequenza è un tipo specifico di iterabile che supporta l’indicizzazione e ha un ordine definito.
# Le sequenze supportano l'indicizzazione
my_list = [10, 20, 30]
print(my_list[0]) # Output: 10
my_string = "Python"
print(my_string[2]) # Output: t
# I set sono iterabili ma NON sono sequenze (niente indicizzazione, nessun ordine garantito)
my_set = {1, 2, 3}
for item in my_set:
print(item) # Funziona - i set sono iterabili
# Ma l'indicizzazione non funziona
try:
print(my_set[0]) # Fallisce - i set non supportano l'indicizzazione
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'set' object is not subscriptableDistinzione chiave: tutte le sequenze (liste, tuple, stringhe, range) sono iterabili, ma non tutti gli iterabili sono sequenze. I set e i dizionari sono iterabili ma non sequenze perché non supportano l’indicizzazione.
35.1.4) Perché l’iterabilità è importante
Capire l’iterabilità ti aiuta a:
- Sapere su cosa puoi eseguire un ciclo: qualsiasi iterabile funziona con i cicli
for - Capire i messaggi di errore: “object is not iterable” significa che non puoi usarlo in un ciclo
for - Usare le comprensioni (comprehensions): le comprensioni di liste, set e dizionari funzionano con qualsiasi iterabile
- Lavorare con le funzioni built-in: molte built-in come
sum(),max(),min()esorted()accettano qualsiasi iterabile
# Tutte queste funzionano perché accettano iterabili
numbers = [1, 2, 3, 4, 5]
print(sum(numbers)) # Output: 15
text = "Python"
print(max(text)) # Output: y (highest alphabetically)
# Funziona anche con i set
unique_values = {10, 5, 20, 15}
print(sorted(unique_values)) # Output: [5, 10, 15, 20]35.2) Iteratori nella vita quotidiana in Python (file, range, dizionari e altro)
35.2.1) Cos’è un iteratore
Un iteratore (iterator) è un oggetto che rappresenta un flusso di dati. Restituisce un valore alla volta quando chiedi l’elemento successivo. Una volta che un iteratore ha restituito tutti i suoi valori, è esaurito e non può essere riutilizzato.
Pensa a un iteratore come a un segnalibro in un libro:
- Ricorda a che punto sei nella sequenza
- Puoi chiedere l’elemento successivo
- Una volta arrivato alla fine, non puoi tornare indietro senza creare un nuovo iteratore
La differenza chiave tra un iterabile e un iteratore:
- Un iterabile (iterable) è qualcosa su cui puoi iterare (come una lista)
- Un iteratore (iterator) è l’oggetto che fa l’iterazione (il meccanismo che avanza attraverso la lista)
# Una lista è un iterabile
numbers = [1, 2, 3]
# Ottenere un iteratore dall'iterabile
iterator = iter(numbers)
# L'iteratore è un oggetto separato
print(type(numbers)) # Output: <class 'list'>
print(type(iterator)) # Output: <class 'list_iterator'>35.2.2) Gli iteratori nei cicli for
Quando scrivi un ciclo for, Python crea automaticamente un iteratore dietro le quinte:
numbers = [10, 20, 30]
# Ciò che scrivi:
for num in numbers:
print(num)
# Ciò che Python fa internamente (concettualmente):
# 1. Chiama iter(numbers) per ottenere un iteratore
# 2. Chiama ripetutamente next() sull'iteratore
# 3. Si ferma quando l'iteratore solleva StopIterationEcco come appare in modo esplicito:
numbers = [10, 20, 30]
# Iterazione manuale (ciò che for fa automaticamente)
iterator = iter(numbers)
try:
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 20
print(next(iterator)) # Output: 30
print(next(iterator)) # Would raise StopIteration
except StopIteration:
print("No more items") # Output: No more itemsIl ciclo for gestisce automaticamente l’eccezione StopIteration, ed è per questo che non la vedi mai nel codice normale.
35.2.3) Oggetti file come iteratori
Gli oggetti file sono ottimi esempi di iteratori. Quando iteri su un file, legge una riga alla volta:
# Creare un file di esempio
with open("students.txt", "w") as file:
file.write("Alice\n")
file.write("Bob\n")
file.write("Charlie\n")
# Leggere il file riga per riga
with open("students.txt", "r") as file:
for line in file:
print(line.strip()) # Output: Alice, Bob, Charlie (on separate lines)Gli oggetti file sono sia iterabili sia iteratori. Restituiscono sé stessi quando chiami iter() su di essi:
with open("students.txt", "r") as file:
iterator = iter(file)
print(file is iterator) # Output: True (same object)
# Leggere le righe manualmente
print(next(iterator)) # Output: Alice
print(next(iterator)) # Output: Bob
print(next(iterator)) # Output: CharlieQuesto è efficiente in termini di memoria perché Python non carica l’intero file in memoria: legge una riga alla volta quando la richiedi.
35.2.4) Oggetti range come iteratori
Gli oggetti range sono iterabili che generano numeri su richiesta:
# Un range è un iterabile
numbers = range(1, 4)
print(type(numbers)) # Output: <class 'range'>
# Ottenere un iteratore dal range
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'range_iterator'>
# Usare l'iteratore
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3I range sono efficienti in termini di memoria perché non memorizzano tutti i numeri in memoria: calcolano ogni numero quando viene richiesto:
# Questo range rappresenta 1 milione di numeri ma usa memoria minima
large_range = range(1000000)
print(type(large_range)) # Output: <class 'range'>
# Ottenere un iteratore
iterator = iter(large_range)
print(next(iterator)) # Output: 0
print(next(iterator)) # Output: 1
# ... può continuare per 1 milione di valori35.2.5) Iteratori dei dizionari
I dizionari forniscono iteratori diversi per chiavi, valori ed elementi:
student = {"name": "Alice", "age": 20, "grade": "A"}
# Iterare sulle chiavi (predefinito)
for key in student:
print(key) # Output: name, age, grade (on separate lines)
# Ottenere esplicitamente un iteratore delle chiavi
keys_iterator = iter(student.keys())
print(next(keys_iterator)) # Output: name
print(next(keys_iterator)) # Output: age
# Iterare sui valori
values_iterator = iter(student.values())
print(next(values_iterator)) # Output: Alice
print(next(values_iterator)) # Output: 20
# Iterare sugli elementi (coppie chiave-valore)
items_iterator = iter(student.items())
print(next(items_iterator)) # Output: ('name', 'Alice')
print(next(items_iterator)) # Output: ('age', 20)35.2.6) Gli iteratori sono esauribili
Una proprietà cruciale degli iteratori è che possono essere usati solo una volta. Una volta esauriti, non si resettano:
numbers = [1, 2, 3]
iterator = iter(numbers)
# Primo passaggio attraverso l'iteratore
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
# L'iteratore ora è esaurito
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("Iterator exhausted") # Output: Iterator exhausted
# Per iterare di nuovo, crea un nuovo iteratore
iterator = iter(numbers)
print(next(iterator)) # Output: 1 (fresh start)Questo è diverso dall’iterabile stesso, che può essere iterato più volte:
numbers = [1, 2, 3]
# Prima iterazione
for num in numbers:
print(num) # Output: 1, 2, 3
# Seconda iterazione (funziona bene - crea un nuovo iteratore)
for num in numbers:
print(num) # Output: 1, 2, 335.3) Usare iter() e next() per avanzare negli iterabili
35.3.1) La funzione iter()
La funzione iter() prende un iterabile e restituisce un iteratore. Questo è il primo passo nel protocollo di iterazione:
# Creare iteratori da diversi iterabili
numbers = [10, 20, 30]
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hi"
text_iterator = iter(text)
print(type(text_iterator)) # Output: <class 'str_iterator'>
my_set = {1, 2, 3}
set_iterator = iter(my_set)
print(type(set_iterator)) # Output: <class 'set_iterator'>Ogni tipo di iterabile restituisce il proprio tipo specializzato di iteratore, ma funzionano tutti allo stesso modo: chiami next() per ottenere il valore successivo.
35.3.2) La funzione next()
La funzione next() recupera l’elemento successivo da un iteratore. Quando non ci sono più elementi, solleva StopIteration:
colors = ["red", "green", "blue"]
iterator = iter(colors)
# Ottenere gli elementi uno alla volta
print(next(iterator)) # Output: red
print(next(iterator)) # Output: green
print(next(iterator)) # Output: blue
# Non ci sono più elementi
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("No more colors") # Output: No more colors35.3.3) Fornire un valore predefinito a next()
Puoi fornire un valore predefinito a next() come secondo argomento. Quando l’iteratore è esaurito, invece di sollevare un’eccezione StopIteration, next() restituirà il valore predefinito che hai specificato:
numbers = [1, 2, 3]
iterator = iter(numbers)
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
print(next(iterator, "Done")) # Output: Done (default value, no exception)
print(next(iterator, "Done")) # Output: Done (still exhausted)Questo è utile quando vuoi gestire la fine dell’iterazione in modo elegante, senza dover ricorrere alla gestione delle eccezioni.
35.4) Creare iteratori personalizzati con iter e next
35.4.1) Perché creare iteratori personalizzati
Gli iterabili integrati (built-in) di Python (liste, stringhe, file) coprono la maggior parte dei casi comuni. Tuttavia, a volte hai bisogno di creare i tuoi oggetti iterabili per un comportamento specializzato:
- Generare sequenze con logica personalizzata
- Iterare su strutture dati che progetti tu
- Creare iterazione efficiente in termini di memoria su grandi dataset
- Implementare valutazione lazy (calcolare i valori solo quando necessario)
Creare un iteratore personalizzato richiede l’implementazione di due metodi speciali: __iter__() e __next__().
35.4.2) Il protocollo dell’iteratore
Per rendere un oggetto un iteratore, deve implementare:
__iter__(): restituisce l’oggetto iteratore stesso (di solitoself)__next__(): restituisce il valore successivo nella sequenza, oppure sollevaStopIterationquando ha finito
class SimpleCounter:
"""An iterator that counts from start to end."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
"""Return the iterator object (self)."""
return self
def __next__(self):
"""Return the next value or raise StopIteration."""
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Using the custom iterator
counter = SimpleCounter(1, 5)
for num in counter:
print(num)
# Output: 1
# Output: 2
# Output: 3
# Output: 4
# Output: 5Vediamo nel dettaglio cosa succede:
- Il ciclo
forchiamaiter(counter), che chiamacounter.__iter__()e ottiene di nuovocounterstesso - Il ciclo chiama ripetutamente
next(counter), che chiamacounter.__next__() - Ogni chiamata a
__next__()restituisce il numero successivo e incrementacurrent - Quando
current > end,__next__()sollevaStopIteratione il ciclo si ferma
35.4.3) Uso manuale degli iteratori personalizzati
Puoi anche usare gli iteratori personalizzati manualmente con iter() e next():
counter = SimpleCounter(10, 13)
# Ottenere l'iteratore (restituisce sé stesso)
iterator = iter(counter)
print(iterator is counter) # Output: True
# Ottenere i valori manualmente
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 11
print(next(iterator)) # Output: 12
print(next(iterator)) # Output: 13
# Ora è esaurito
try:
print(next(iterator))
except StopIteration:
print("Counter exhausted") # Output: Counter exhausted35.4.4) Gli iteratori sono esauribili (ripasso)
Ricorda che gli iteratori possono essere usati solo una volta:
counter = SimpleCounter(1, 3)
# Prima iterazione
for num in counter:
print(num) # Output: 1, 2, 3
# Seconda iterazione (non funziona - l'iteratore è esaurito)
for num in counter:
print(num) # Nothing printed - iterator is already exhaustedPer iterare di nuovo, devi creare una nuova istanza:
# Crea un nuovo contatore per ogni iterazione
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3 (new iterator)35.4.5) Creare una classe iterabile (non solo un iteratore)
Spesso vuoi una classe che sia iterabile ma che crei un iteratore nuovo ogni volta. Per farlo, separa l’iterabile dall’iteratore:
class CounterIterable:
"""An iterable that creates fresh counter iterators."""
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
"""Return a new iterator each time."""
return CounterIterator(self.start, self.end)
class CounterIterator:
"""The actual iterator that does the counting."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# Now we can iterate multiple times
counter = CounterIterable(1, 3)
# First iteration
for num in counter:
print(num) # Output: 1, 2, 3
# Second iteration (works because __iter__ creates a new iterator)
for num in counter:
print(num) # Output: 1, 2, 3Questo pattern separa le responsabilità:
CounterIterableè l’iterabile: sa come creare iteratoriCounterIteratorè l’iteratore: sa come avanzare tra i valori
35.4.6) Esempio pratico: iterare su una struttura dati personalizzata
Creiamo un iteratore per una struttura dati personalizzata: una semplice playlist:
class Playlist:
"""A music playlist that can be iterated over."""
def __init__(self):
self.songs = []
def add_song(self, title, artist):
"""Add a song to the playlist."""
self.songs.append({"title": title, "artist": artist})
def __iter__(self):
"""Return an iterator for the playlist."""
return PlaylistIterator(self.songs)
class PlaylistIterator:
"""Iterator for stepping through songs in a playlist."""
def __init__(self, songs):
self.songs = songs
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.songs):
raise StopIteration
song = self.songs[self.index]
self.index += 1
return song
# Using the playlist
playlist = Playlist()
playlist.add_song("Imagine", "John Lennon")
playlist.add_song("Bohemian Rhapsody", "Queen")
playlist.add_song("Hotel California", "Eagles")
# Iterate over songs
print("Now playing:")
for song in playlist:
print(f" {song['title']} by {song['artist']}")
# Output: Now playing:
# Output: Imagine by John Lennon
# Output: Bohemian Rhapsody by Queen
# Output: Hotel California by Eagles
# Can iterate again (creates a new iterator)
print("\nReplay:")
for song in playlist:
print(f" {song['title']}")
# Output: Replay:
# Output: Imagine
# Output: Bohemian Rhapsody
# Output: Hotel California35.4.7) Quando usare iteratori personalizzati
Crea iteratori personalizzati quando:
- Hai bisogno di valutazione lazy: generare valori su richiesta invece di memorizzarli tutti
- Hai una struttura dati personalizzata: renderla iterabile così funziona con i cicli
for - Hai bisogno di una logica di iterazione speciale: saltare elementi, trasformare valori o implementare avanzamenti complessi
- Conta l’efficienza di memoria: generare grandi sequenze senza memorizzarle
Tuttavia, nel Capitolo 36 imparerai i generatori (generators), che offrono un modo molto più semplice per creare iteratori usando la keyword yield. I generatori sono di solito preferiti rispetto all’implementazione manuale di __iter__() e __next__() perché sono più concisi e più facili da capire.
Capire come creare iteratori personalizzati ti dà una visione di come funziona il protocollo di iterazione di Python, anche se spesso userai invece i generatori. I concetti che hai imparato qui—__iter__(), __next__() e StopIteration—sono fondamentali per comprendere i generatori e altre tecniche avanzate di iterazione nel prossimo capitolo.