Python & AI Tutorials Logo
Programmazione Python

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:

python
# 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():

python
# 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 iterable

Quando 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.

python
# 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 subscriptable

Distinzione 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.

Oggetti Python

Iterabili

Non iterabili

Sequenze

Iterabili non-sequenza

Liste, tuple, stringhe, range

Set, dizionari, file

Numeri, None, booleani

35.1.4) Perché l’iterabilità è importante

Capire l’iterabilità ti aiuta a:

  1. Sapere su cosa puoi eseguire un ciclo: qualsiasi iterabile funziona con i cicli for
  2. Capire i messaggi di errore: “object is not iterable” significa che non puoi usarlo in un ciclo for
  3. Usare le comprensioni (comprehensions): le comprensioni di liste, set e dizionari funzionano con qualsiasi iterabile
  4. Lavorare con le funzioni built-in: molte built-in come sum(), max(), min() e sorted() accettano qualsiasi iterabile
python
# 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)
python
# 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:

python
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 StopIteration

Ecco come appare in modo esplicito:

python
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 items

Il ciclo for gestisce automaticamente l’eccezione StopIteration, ed è per questo che non la vedi mai nel codice normale.

No -> StopIteration

for item in iterable:

Python chiama iter(iterable)

Ottiene l'oggetto iteratore

Python chiama next(iterator)

Altri elementi?

Assegna a item

Esegue il corpo del ciclo

Esce dal ciclo

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:

python
# 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:

python
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: Charlie

Questo è 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:

python
# 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: 3

I range sono efficienti in termini di memoria perché non memorizzano tutti i numeri in memoria: calcolano ogni numero quando viene richiesto:

python
# 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 valori

35.2.5) Iteratori dei dizionari

I dizionari forniscono iteratori diversi per chiavi, valori ed elementi:

python
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:

python
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:

python
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, 3

35.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:

python
# 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:

python
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 colors

35.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:

python
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:

  1. __iter__(): restituisce l’oggetto iteratore stesso (di solito self)
  2. __next__(): restituisce il valore successivo nella sequenza, oppure solleva StopIteration quando ha finito
python
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: 5

Vediamo nel dettaglio cosa succede:

  1. Il ciclo for chiama iter(counter), che chiama counter.__iter__() e ottiene di nuovo counter stesso
  2. Il ciclo chiama ripetutamente next(counter), che chiama counter.__next__()
  3. Ogni chiamata a __next__() restituisce il numero successivo e incrementa current
  4. Quando current > end, __next__() solleva StopIteration e il ciclo si ferma

35.4.3) Uso manuale degli iteratori personalizzati

Puoi anche usare gli iteratori personalizzati manualmente con iter() e next():

python
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 exhausted

35.4.4) Gli iteratori sono esauribili (ripasso)

Ricorda che gli iteratori possono essere usati solo una volta:

python
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 exhausted

Per iterare di nuovo, devi creare una nuova istanza:

python
# 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:

python
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, 3

Questo pattern separa le responsabilità:

  • CounterIterable è l’iterabile: sa come creare iteratori
  • CounterIterator è 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:

python
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 California

35.4.7) Quando usare iteratori personalizzati

Crea iteratori personalizzati quando:

  1. Hai bisogno di valutazione lazy: generare valori su richiesta invece di memorizzarli tutti
  2. Hai una struttura dati personalizzata: renderla iterabile così funziona con i cicli for
  3. Hai bisogno di una logica di iterazione speciale: saltare elementi, trasformare valori o implementare avanzamenti complessi
  4. 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.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai