W3docs

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 cicli for e altri contesti di iterazione.
    • __next__() — restituisce il valore successivo ogni volta che viene chiamato. Quando non rimangono valori, solleva StopIteration.

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.

python— editable, runs on the server
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| E

Come 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
# 1

Nota 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) — chiama obj.__iter__() e restituisce l'iteratore risultante.
  • next(it) — chiama it.__next__() e restituisce il valore successivo.
  • next(it, default) — restituisce default invece di sollevare StopIteration quando 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 exhausted

Se 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  5

zip()

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

enumerate()

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

map() 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 iterables

Ogni iteratore è anche un iterable (perché __iter__ restituisce self), ma non ogni iterable è un iteratore.

Quando usare iteratori vs. liste

SituazioneUsa
Necessità di accesso casuale (items[5])list
Necessità di iterare una volta, la memoria è importanteiteratore / generator
Sequenze infinite o molto grandiiteratore / generator
Necessità di iterare più voltelist (mantieni l'originale)
Pipeline di trasformazioniiteratori 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().

Esercitazione

Pratica
Which methods make up the Python iterator protocol?
Which methods make up the Python iterator protocol?
Was this page helpful?