Iteratori Python
Scopri come funzionano gli iteratori Python, come creare classi iteratori personalizzate e quando preferirli alle liste.
Un iteratore è una delle astrazioni fondamentali di Python. Ogni volta che scrivi un ciclo for, chiami zip() o usi una list comprehension, Python si affida silenziosamente al protocollo degli iteratori. Questo capitolo spiega cosa sono gli iteratori, come costruirne di personalizzati, come usare il ricco insieme di iteratori integrati e quando gli iteratori sono lo strumento giusto per il lavoro.
Cos'è un iteratore?
Python distingue tra due concetti correlati:
- Un iterable è qualsiasi oggetto su cui puoi ciclare — una
list,tuple,str,dict,set, o qualsiasi oggetto la cui classe definisce__iter__. Può produrre un iteratore, ma non tiene traccia della posizione da solo. - Un iteratore è un oggetto che tiene traccia dello stato di attraversamento. Implementa due metodi che insieme formano il protocollo degli iteratori:
__iter__()— restituisce l'oggetto iteratore stesso. Questo consente agli iteratori di funzionare all'interno di ciclifore altri contesti di iterazione.__next__()— restituisce il valore successivo ogni volta che viene chiamato. Quando non rimangono valori, sollevaStopIteration.
La differenza chiave: puoi ciclare su una lista quante volte vuoi perché ogni ciclo for richiede un iteratore fresco. Un iteratore è monodirezionale e monouso — una volta esaurito, chiamare next() su di esso solleva sempre StopIteration.
graph LR
A[Iterator Object] --> B[__iter__]
B --> C[Returns self]
A --> D[__next__]
D --> E[Next Value]
D --> F{No values left?}
F -->|Yes| G[Raises StopIteration]
F -->|No| ECome un ciclo for usa gli iteratori
Il ciclo for è solo zucchero sintattico per il protocollo degli iteratori. Internamente, Python traduce:
for item in some_iterable:
print(item)in circa questo:
_it = iter(some_iterable) # call __iter__()
while True:
try:
item = next(_it) # call __next__()
except StopIteration:
break
print(item)Capire questa traduzione chiarisce perché qualsiasi oggetto che implementa __iter__ e __next__ funziona perfettamente in un ciclo for, con zip(), enumerate() e qualsiasi altro contesto che si aspetta un iterable.
Costruire un iteratore personalizzato
Per creare un iteratore personalizzato, definisci una classe che implementa sia __iter__ che __next__. Ecco un iteratore Countdown che conta a ritroso da un numero dato fino a 1:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self # the iterator is its own iterable
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(5):
print(n)
# Output:
# 5
# 4
# 3
# 2
# 1Nota che __iter__ restituisce self. Questo è ciò che consente di inserire lo stesso oggetto direttamente in un ciclo for — il ciclo chiama iter() su di esso, che chiama __iter__(), che restituisce l'iteratore stesso.
Aggiungere un parametro step
Puoi aggiungere qualsiasi logica all'interno di __next__. Ecco un iteratore StepRange che imita range() ma accetta un valore di step:
class StepRange:
def __init__(self, start, stop, step=1):
self.current = start
self.stop = stop
self.step = step
def __iter__(self):
return self
def __next__(self):
if self.current >= self.stop:
raise StopIteration
value = self.current
self.current += self.step
return value
print(list(StepRange(0, 10, 3)))
# Output: [0, 3, 6, 9]Chiamare list() su qualsiasi iteratore lo esaurisce e raccoglie tutti i valori in una lista — un pattern utile quando hai bisogno di tutti i risultati in una volta.
Le funzioni integrate iter() e next()
Le funzioni integrate iter() e next() sono il modo standard per lavorare direttamente con il protocollo degli iteratori.
iter(obj)— chiamaobj.__iter__()e restituisce l'iteratore risultante.next(it)— chiamait.__next__()e restituisce il valore successivo.next(it, default)— restituiscedefaultinvece di sollevareStopIterationquando l'iteratore è esaurito. Questo è il modo più sicuro per esaminare il prossimo elemento senza un blocco try/except.
words = ["hello", "world"]
it = iter(words)
print(next(it)) # hello
print(next(it)) # world
print(next(it, "done")) # done (exhausted; returns default)La forma a due argomenti di next() è particolarmente utile negli scenari di streaming o parsing in cui si vuole gestire la fine dell'input in modo elegante.
Gli iteratori sono monouso
Questa è la trappola più comune con gli iteratori: una volta esaurito, un iteratore non può essere riavvolto.
it = iter([1, 2, 3])
for x in it:
print(x) # prints 1, 2, 3
for x in it:
print(x) # prints nothing — iterator is exhaustedSe hai bisogno di iterare più volte, mantieni l'iterable originale (ad esempio la lista) e chiama di nuovo iter(), oppure usa una list comprehension per materializzare prima tutti i valori.
Funzioni integrate che restituiscono iteratori
La libreria standard di Python è costruita sugli iteratori. Queste funzioni restituiscono tutte iteratori anziché liste, quindi sono efficienti in termini di memoria anche su sequenze molto grandi:
range()
range(start, stop, step) restituisce un iteratore di interi. Non memorizza gli interi in memoria — calcola ciascuno su richiesta.
for i in range(1, 6):
print(i)
# Output: 1 2 3 4 5zip()
zip() prende più iterables e restituisce un iteratore di tuple, abbinando gli elementi posizione per posizione. L'iterazione si ferma all'input più corto.
names = ["Alice", "Bob", "Carol"]
scores = [95, 88, 72]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Output:
# Alice: 95
# Bob: 88
# Carol: 72enumerate()
enumerate() avvolge qualsiasi iterable e restituisce coppie (indice, valore). Usalo per evitare di mantenere una variabile contatore manuale.
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# Output:
# 1. apple
# 2. banana
# 3. cherrymap() e filter()
Entrambe le funzioni restituiscono iteratori (in Python 3). map(fn, iterable) applica una funzione a ogni elemento; filter(fn, iterable) mantiene solo gli elementi per cui la funzione restituisce True.
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
print(doubled) # [2, 4, 6, 8, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]Verificare se un oggetto è un iteratore
Usa isinstance() con le classi base astratte del modulo collections.abc per testare la capacità di iterazione e lo stato di iteratore:
from collections.abc import Iterable, Iterator
my_list = [1, 2, 3]
my_iter = iter(my_list)
print(isinstance(my_list, Iterable)) # True — list is iterable
print(isinstance(my_list, Iterator)) # False — list is NOT an iterator
print(isinstance(my_iter, Iterator)) # True — list_iterator is an iterator
print(isinstance(my_iter, Iterable)) # True — all iterators are also iterablesOgni iteratore è anche un iterable (perché __iter__ restituisce self), ma non ogni iterable è un iteratore.
Quando usare iteratori vs. liste
| Situazione | Usa |
|---|---|
Necessità di accesso casuale (items[5]) | list |
| Necessità di iterare una volta, la memoria è importante | iteratore / generator |
| Sequenze infinite o molto grandi | iteratore / generator |
| Necessità di iterare più volte | list (mantieni l'originale) |
| Pipeline di trasformazioni | iteratori concatenati (map, filter, itertools) |
Per dataset di grandi dimensioni — leggere milioni di righe da un file, elaborare dati in streaming — un iteratore evita di caricare tutto in memoria in una volta. Per collezioni piccole e finite in cui si accede agli elementi ripetutamente, una lista è più semplice.
Iteratori vs. Generator
Un generator è una comoda abbreviazione per scrivere un iteratore. Invece di una classe con __iter__ e __next__, scrivi una funzione che usa yield. Python la converte automaticamente in un iteratore.
# Iterator class
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
# Equivalent generator function
def countdown(start):
while start > 0:
yield start
start -= 1
print(list(countdown(5))) # [5, 4, 3, 2, 1]Usa un iteratore basato su classe quando hai bisogno di metodi aggiuntivi o di uno stato mutabile oltre a ciò che un semplice generator offre. Usa un generator nella maggior parte degli altri casi — è più conciso ed egualmente potente.
Vedi il capitolo Python Generators per un trattamento completo di yield, espressioni generator e send().