in Tutorial Python

Elementi di Python: Iteratori e Generatori

Una delle caratteristiche per cui python è più conosciuto ed amato dalla community degli utenti amatoriali e non, è data dal suo approccio intuitivo al concetto di iterazione. Tutti i linguaggi di programmazione offrono dei costrutti più o meno classici per effettuare delle operazioni secondo uno schema iterativo, generalmente con istruzioni come whilefor: python segue questa tradizione permettendo l’utilizzo di queste stesse due istruzioni che funzionano nella maniera in cui ci si aspetta venendo da altri linguaggi, o quasi.

Tra queste due istruzioni, il for ha un posto speciale nella sintassi di python, in quanto il linguaggio ha come caratteristica nativa la possibilità di iterare in maniera semplice ed efficace sulle sue collezioni di dati (liste, dizionari, tuple) nella seguente maniera:

lista_prezzi = [12, 24, 25.6, 2]
for prezzo in lista_prezzi:
    print(prezzo)

Il punto di forza di questo tipo di iterazione è dato dalla forte espressività che possiede: chiunque guardando queste poche linee di codice, può capire cosa sta accadendo. La possibilità di potere iterare in questo modo su strutture dati non è unica di python: C++, Java ed altri linguaggi molto utilizzati hanno nel tempo adottato questo tipo di approccio ai cicli.

Queste strutture sono dette iterabili. Un altro esempio di struttura iterabile è il tipo stringa. In generale, dato un iterabile, si può ottenere un iteratore su quella struttura utilizzando la parola chiave iter, e si può navigare la struttura con la parola chiave next. Inoltre, su un iterabile è possibile utilizzare un ciclo for come è stato fatto nel precedente esempio, sfruttando la parola chiave in:

# In generale
ogg = ... #ogg iterabile per ipotesi
iteratore = iter(ogg)
next(iteratore_ogg)

# Esempio pratico
saluto = "ciao"
iteratore_saluto = iter(saluto)
next(iteratore_saluto)
next(iteratore_saluto)
next(iteratore_saluto)
next(iteratore_saluto)

# che è equivalente a

for lettera in saluto:
print(lettera)

Il codice qui sopra produce il seguente output:

> c
> i
> a
> o

A questo punto ci si può chiedere: c’è qualche modo per utilizzare questo approccio con strutture o, per essere più corretti, classi realizzate personalmente? Python fornisce una semplice maniera di implementare la funzionalità di iteratore in una classe qualsiasi, tramite l’utilizzo dei metodi speciali __iter____next__. Una classe che implementa questi due metodi può essere utilizzata con le parole chiave iter e next come visto nell’esempio precedente.  

Si consideri il seguente esempio, ossia una semplice implementazione di una Lista Concatenata, una struttura dati in cui ogni elemento di una lista punta all’elemento successivo, fino all’ultimo che è caratterizzato per puntare a None:

class ListaConcatenata:
    class Nodo:
        val = None
        prossimo = None

    def __init__(self):
        self.testa = None

    def aggiungi(self, n_val):
        if self.testa is None:
            n_nodo = self.Nodo()
            n_nodo.val = n_val
            self.testa = n_nodo
            return
        elem = self.testa
        while elem.prossimo is not None:
            elem = elem.prossimo

n_nodo = self.Nodo()
n_nodo.val = n_val
elem.prossimo = n_nodo

Per scorrere gli elementi di questa classe, al momento, si può solo utilizzare un costrutto del tipo:

lista_conc = ListaConcatenata()

...
# riempio la lista
...

corrente = lista_conc.testa
while corrente is not None:
    print(corrente.val)
    corrente = corrente.prossimo

Per avere accesso al costrutto for di Python, si procede con l’implementare un meccanismo di iterazione in questa classe, nella seguente maniera:

def __iter__(self):
    self.corrente = self.testa
    return self

def __next__(self):
    if self.corrente is None:
        raise StopIteration
    val_corrente = self.corrente.val
  self.corrente = self.corrente.prossimo
    return val_corrente

# Si può ora utilizzare il costrutto for, sfruttando l'implementazione di __iter__ e __next__ lista_conc = ListaConcatenata() # riempio la lista con numeri fino a 10 (escluso) for elem in range(10): lista_conc.aggiungi(elem)

# stampo la lista for elem in lista_conc: print(elem)

Si notano un paio di questioni:

  • Il metodo __iter__ ha il compito di inizializzare l’iterazione, in questo caso si definisce un punto di inizio (self.corrente viene introdotto come segnaposto per la navigazione fra gli elementi) considerando l’elemento in testa, dopodiché bisogna restituire l’iteratore stesso, tramite la parola chiave self;
  • il metodo __next__ effettua l’operazione di navigazione vera e propria, implementando la regola per il singolo hop (salto) da un elemento all’altro; quando si raggiunge la fine, bisogna farlo sapere all’iteratore tramite il lancio di una eccezione StopIteration.

Python, però, offre anche un altro metodo per risolvere il problema dell’iterazione: quello dei generatori.

Un generatore è una funzione che ha un comportamento simile a quello di un iteratore, ossia può essere iterata e la funzione next può essere chiamata su di essa. A differenza di una classe che implementa il protocollo di iterazione discusso precedentemente, un generatore ha alcune proprietà particolari:

  • Un generatore è un iteratore ma non è vero il viceversa (il tipo generatore è una specializzazione di un iteratore);
  • Un generatore è un tipo di funzione lazy: i valori su cui si itera vengono generati al volo quando si richiede una next sulla sequenza, il che significa che tali valori non gravano in memoria come può accadere con un iteratore classico (soprattutto se sono molti o fanno parte di una sequenza infinita!);
  • Come conseguenza diretta del punto precedente, si può iterare sui valori di un generatore solo una volta.

Per potere realizzare una funzione che sia un generatore, si utilizza la parola chiave python yield, che agisce in maniera simile alla parola chiave return: yield fa sì che la funzione ritorni un valore, ma senza interrompere il ciclo di vita della funzione. Si possono avere multiple yield nel corpo di una funzione e sarà possibile iterare sulla funzione finché non si raggiunge l’ultima yield.
Qui di seguito un esempio di un generatore, che restituisce tutti i numeri pari fino ad un certo numero passato come argomento della funzione:

def pari_fino_a(num):
    for counter in range(num):
        if counter%2 == 0:
            yield counter

# Stampa la sequenza
ultimo = 12
for num in pari_fino_a(ultimo):
    print(num)

La possibilità di realizzare oggetti iterabili su sequenze di questo tipo rappresenta uno strumento molto potente, in quanto fornisce un metodo per ottimizzare l’uso della memoria di un programma python e, non da meno, fornisce un’alternativa all’utilizzo delle classi nel caso in cui sia possibile, in quanto i generatori hanno la possibilità, come appena visto nell’esempio, di mantenere uno stato interno.

Ricapitolando, gli iteratori sono degli strumenti utilissimi e fondamentali quando si ha a che fare con collezioni di dati o sequenze; come regola intuitiva, se si ha solo bisogno di una sequenza da iterare, è sempre meglio lavorare con un generatore (per il minore overhead in memoria); se si ha invece a che fare con strutture su cui può tornare utile invocare altre funzioni o che hanno bisogno di essere persistenti, sempre pronte in memoria, è meglio utilizzare una classe e dotarla delle funzionalità di un iteratore.

Gli esempi trattati, con del codice per testarli, sono disponibili al seguente indirizzo. Grazie per la lettura, al prossimo articolo di Elementi di Python.

Scrivi un commento

Commento

  1. ottima spiegazione, presenta l’argomento in modo molto semplice e con dei riferimenti puntuali. L’esempio relativo alla lista concatenata, nel mio VSCODE presenta un errore di indentazione per le ultime tre righe.
    Per i principianti come me sarebbe di aiuto poter avere un esempio completo e che presenti dopo il RUN un risultato visibile. Questo perché chi come me fa delle precise ricerche su particolari funzionalità e ha poca sicurezza nella sintassi possa analizzando i risultati meglio comprendere le funzioni che stava cercando.

    • Ciao, mi fa davvero tanto piacere che l’articolo ti sia piaciuto e ti ringrazio molto per il feedback offerto! Gli errori legati all’errata indentazione, dovuti ad una svista nel ricopiare il codice, sono stati corretti e, come da te giustamente richiesto, ho raggruppato gli esempi presentati assieme a delle istruzioni che li pongono in esecuzione. Il link contenente il codice è stato aggiunto all’articolo, puoi reperire il tutto qui:

      https://gist.github.com/Abathargh/de7062001a70c2f4f71fce4cdf9f9ec3
      Spero che la tua esperienza su antima possa continuare ad essere positiva, grazie ancora!