Domanda dubbio su iteratori python

TheWorm91

Helper
31 Marzo 2022
428
49
204
317
Stavo ripassando le classi e la programmazione a oggetti su python (che già avevo studiato su java), ho visto che ci sono delle classi "di tipo" iteratore

Mi sono imbattuto in questo esempio che stampa i numeri pari di una lista ma non ho ben chiaro alcuni concetti:

Python:
numeri = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
class Pari:
    def __init__(self, numeri):
        self.numeri = numeri
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        while self.index < len(self.numeri):
            if self.numeri[self.index] % 2 == 0:
                valore = self.numeri[self.index]
                self.index += 1
                return valore
            else:
                self.index += 1
        raise StopIteration

iteratore_pari = Pari(numeri)

for numero in iteratore_pari:
    print(numero)

Se non ho capito male con __init__ inizializzo le variabili dell'oggetto passando il valore della lista da scorrere mentre il metodo __next__ viene richiamato ed eseguito implicitamente durante il ciclo for
Non ho capito invece __iter__ a cosa serva...
Mi domando perchè creare una classe iteratore quando si sarebbe potuto realizzare un codice simile:
Python:
numeri = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for numero in numeri:
    if numero % 2 == 0: print(numero)
oppure creare una classe StampaLista con una def stampaPari(lista) che esegue la stampa dei numeri con la possibilità di aggiungere nella classe un'altra def stampaDispari(lista)?
Non so se mi sono spiegato bene ma sono un po' confuso su questo argomento
 
Se non ho capito male con __init__ inizializzo le variabili dell'oggetto passando il valore della lista da scorrere
Sì, __init__ è il costruttore. Il costruttore è il metodo (i.e., la funzione associata ad una classe) che ti permette di creare un oggetto in uno stato valido.

Non ha senso creare un quadrato senza specificare la lunghezza di un lato, non puoi creare uno studente senza specificare nome, cognome e matricola, non puoi creare una matrice senza specificare la sua dimensione e i valori che contiene e, nel tuo caso, non puoi scorrere i numeri pari se non hai niente da scorrere. Nei casi in cui non ti interessa passare niente al costruttore (e.g., stai creando una linked list e vuoi sempre partire dalla lista vuota) potrai scrivere def __init__(self)) ma, presumibilmente, dovrai comunque inizializzare alcune "variabile all'interno della classe" (aka., campi) con un valore ragionevole.

il metodo __next__ viene richiamato ed eseguito implicitamente durante il ciclo for
Non ho capito invece __iter__ a cosa serva...
I metodi __next__ e __iter__ sono quelli che ti permettono di usare il ciclo for. In java diremmo che sono i metodi che sono richiesti dall'interfaccia Iterable. In particolare, __iter__ è il metodo che il ciclo for usa per ottenere un iteratore e __next__ è il metodo che usa per passare al prossimo elemento. Hai bisogno di un metodo __iter__ perché spesso non hai direttamente a che fare con gli iteratori, ma vuoi ottenerne uno da un oggetto di tipo diverso. Per esempio, supponiamo che stai creando una classe per creare un alberi binari
Python:
class BinaryTree:
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None
    # TODO: altri metodi
Se volessi usare il ciclo for per stampare il valore di ogni nodo dovrei creare un iteratore, però ho vari modi per scorrere i nodi: in-order, pre-order, post-order e level-order. Potrei fare una cosa di questo tipo:
Python:
class BinaryTree:
    def __init__(self, data):
        self.data = data
        self.left = None
        self.right = None
    def inorder(self):
        return InOrderIterator(self)
    def preorder(self):
        return PreOrderIterator(self)
    def __iter__(self):
        return self.inorder()
    # TODO: altri metodi (nota: non ho bisogno di __next__)

class InOrderIterator:
    def __init__(self, binarytree): # TODO implementazione
    def __iter__(self): # TODO implementazione
    def __next__(self): # TODO implementazione

class PreOrderIterator:
    def __init__(self, binarytree): # TODO implementazione
    def __iter__(self): # TODO implementazione
    def __next__(self): # TODO implementazione
Il punto fondamentale è che la logica dell'iteratore, ovvero il codice che salva il punto in cui sono arrivato adesso e mi permette di passare al nodo successivo, è da implementare all'interno di InOrderIterator e PreOrderIterator e non all'inteno di BinaryTree. Gli alberi binari sono costituiti da un dato, un nodo sinistro e un nodo destro. Se vuoi scorrere i vari nodi dovrai salvarti altre variabili, ma queste variabili non hanno niente a che fare con il concetto di albero binario. Tuttavia, è utile scorrere i nodi di un albero binario e quindi implementi un metodo __iter__ che ritorna l'iteratore di default (nel mio caso, la visita in-order) ed è utile avere altri metodi che ti permettono di scorrere i nodi in modo diverso. Una volta completata la classe, potrai fare una cosa del genere
Python:
tree = BinaryTree(...)
for node in tree: print(node)
for node in tree.inorder(): print(node)
for node in tree.preorder(): print(node)
In particolare, ti faccio notare che in for node in tere: print(node) viene chiamato il metodo il metodo __iter__ di BinaryTree e poi viene chiamato per tante volte il metodo __next__ di InOrderIterator.

Mi domando perchè creare una classe iteratore quando si sarebbe potuto realizzare un codice simile:
Python:
numeri = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for numero in numeri:
if numero % 2 == 0: print(numero)
oppure creare una classe StampaLista con una def stampaPari(lista) che esegue la stampa dei numeri con la possibilità di aggiungere nella classe un'altra def stampaDispari(lista)?
Penso che dopo l'esempio che ti ho fatto sia diventato ovvio. Se vuoi scorrere i numeri pari ti basta un if, ma se vuoi attraversare un albero binario con una visita in-order (o in qualunque altro modo) devi scriverti una spataffiata di codice che è meglio implementare una volta sola e averla lì per sempre piuttosto che reinventartela di volta in volta. Perché non scrivere un metodo stampaInOrder() invece di un iteratore? Perché non necessariamente vuoi stampare i vari nodi, magari vuoi fare una cosa completamente diversa. Io all'interno del for ho scritto print(node), ma magari tu stai usando il ciclo for per prendere il massimo, per incrementare il valore di ogni nodo, o per fare una cosa completamente diversa.
 
  • Grazie
Reazioni: TheWorm91