4.5. UserDict: una classe wrapper

Come avete visto, FileInfo è una classe che agisce come un dizionario. Per esplorare ulteriormente questo aspetto, diamo uno sguardo alla classe UserDict nel modulo UserDict, che è l'antenato della nostra classe FileInfo. Non è nulla di speciale; la classe è scritta in Python e memorizzata in un file .py, proprio come il nostro codice. In particolare, è memorizzata nella directory lib della vostra installazione di Python.

Suggerimento
Nella IDE di Python su Windows, potete aprire rapidamente qualunque modulo nel vostro library path usando File->Locate... (Ctrl-L).

Nota storica.  Nelle versioni di Python antecedenti la 2.2, non potevate ereditare direttamente dai tipi built-in come stringhe, liste e dizionari. Per compensare a tale mancanza, Python viene rilasciato con delle classi wrapper che mimano il comportamento di questi tipi built-in: UserString, UserList e UserDict. Usando una combinazione di metodi normali e speciali, la classe UserDict fa un'eccellente imitazione di un dizionario, ma è semplicemente una classe come qualunque altra, così potete ereditare da essa per creare delle classi personalizzate che si comportano come dizionari, come abbiamo fatto con FileInfo. In Python 2.2 o superiore, potreste riscrivere l'esempio di questo capitolo in modo che FileInfo erediti direttamente da dict invece che da UserDict. Comunque dovreste lo stesso leggere come funziona UserDict, nel caso abbiate bisogno di implementare questo genere di oggetto wrapper o nel caso abbiate bisogno di supportare versioni di Python antecedenti alla 2.2.

Esempio 4.11. Definire la classe UserDict


class UserDict:                                1
    def __init__(self, dict=None):             2
        self.data = {}                         3
        if dict is not None: self.update(dict) 4 5
1 Notate che UserDict è una classe base, non eredita da nessun'altra classe.
2 Questo è il metodo __init__ che andiamo a sovrascrivere nella classe FileInfo. Notate che la lista degli argomenti in questa classe antenato è diversa dai suoi discendenti. Va bene; ogni sottoclasse può avere il suo insieme di argomenti, finché chiama l'antenato con gli argomenti esatti. Qui la classe antenato ha un modo per definire i valori iniziali (passando un dizionario nell'argomento dict) di cui la nostra FileInfo non si avvantaggia.
3 Python supporta gli attributi (chiamati “variabili d'istanza” in Java ed in Powerbuilder, “variabili membro” in C++), cioè dati mantenuti da una specifica istanza di una classe. In questo caso, ogni istanza di UserDict avrà un attributo data. Per referenziare questo attributo dal codice esterno alla classe, lo dovrete qualificare usando il suo nome di istanza, instance.data, nello stesso modo in cui qualificate una funzione con il nome del suo modulo. Per referenziare un attributo all'interno della classe, usiamo self come qualificatore. Per convenzione, tutti gli attributi sono inizializzati con dei valori ragionevoli nel metodo __init__. Ad ogni modo, questo non è richiesto, in quanto gli attributi, così come le variabili locali, cominciano ad esistere nel momento in cui viene loro assegnato un valore.
4 Il metodo update è un duplicatore di dizionario: copia tutte le chiavi ed i valori da un dizionario ad un altro. Questo metodo non cancella il dizionario di destinazione, se il dizionario di destinazione ha già alcune delle chiavi, il loro valore sarà sovrascritto, ma gli altri dati rimarranno invariati. Pensate al metodo update come ad una funzione di fusione e non di copia.
5 Questa è una sintassi che potreste non aver ancora visto (non l'ho usata negli esempi di questo libro). Si tratta di una istruzione if, ma invece di avere un blocco indentato nella riga successiva, c'è semplicemente una singola istruzione sulla stessa riga, dopo i due punti. È una sintassi perfettamente legale ed è semplicemente una scorciatoia quando avete una sola istruzione in un blocco. (È come specificare una singola istruzione senza graffe in C++). Potete usare questa sintassi o potete usare il codice indentato in righe successive, ma non potete fare entrambe le cose per lo stesso blocco.
Nota
Java e Powerbuilder supportano l'overload di funzioni per lista di argomenti, cioè una classe può avere più metodi con lo stesso nome, ma diverso numero di argomenti o argomenti di tipo diverso. Altri linguaggi (principalmente PL/SQL) supportano l'overload di funzioni per nome di argomento; cioè una classe può avere più metodi con lo stesso nome e lo stesso numero di argomenti dello stesso tipo, ma i nomi degli argomenti sono diversi. Python non supporta nessuno di questi; non ha nessuna forma di overload di funzione. I metodi sono definiti solamente dal loro nome e ci può essere solamente un metodo per ogni classe con un dato nome. Così, se una classe discendente ha un metodo __init__, questo sovrascrive sempre il metodo __init__ dell'antenato, anche se il discendente lo definisce con una lista di argomenti diversa. La stessa regola si applica ad ogni altro metodo.
Nota
Guido, l'autore originale di Python, spiega l'override dei metodi in questo modo: "Classi derivate possono sovrascrivere i metodi delle loro classi base. Siccome i metodi non hanno privilegi speciali quando chiamano altri metodi dello stesso oggetto, un metodo di una classe base che chiama un altro metodo definito nella stessa classe base, può infatti finire per chiamare un metodo di una classe derivata che lo sovrascrive. (Per i programmatori C++ tutti i metodi in Python sono effettivamente virtual.)" Se questo per voi non ha senso (confonde un sacco pure me), sentitevi liberi di ignorarlo. Penso che lo capirò più avanti.
Nota
Assegnate sempre un valore iniziale a tutti gli attributi di un'istanza nel metodo __init__. Vi risparmierà ore di debugging più tardi, spese a tracciare tutte le eccezioni AttributeError che genera il programma perché state referenziando degli attributi non inizializzati (e quindi inesistenti).

Esempio 4.12. Metodi comuni di UserDict

    def clear(self): self.data.clear()          1
    def copy(self):                             2
        if self.__class__ is UserDict:          3
            return UserDict(self.data)         
        import copy                             4
        return copy.copy(self)                 
    def keys(self): return self.data.keys()     5
    def items(self): return self.data.items()  
    def values(self): return self.data.values()
1 clear è un normale metodo della classe; è pubblicamente disponibile per essere chiamato da chiunque in qualunque momento. Notate che clear, come tutti gli altri metodi della classe, ha self come primo argomento (ricordate, non siete voi ad includere self quando chiamate un metodo; è qualcosa che Python fa già per voi). Notate inoltre la tecnica base di questa classe wrapper: memorizza un vero dizionario (data) come attributo, definisce tutti i metodi che ha un vero dizionario e redireziona tutti questi metodi verso i loro corrispondenti del vero dizionario (nel caso lo aveste dimenticato, il metodo clear di un dizionario cancella tutte le sue chiavi ed i valori ad esse associati).
2 Il metodo copy di un vero dizionario restituisce un nuovo dizionario che è un esatto duplicato di quello originale (tutte le stesse coppie chiave-valore). Ma UserDict non può semplicemente redirezionare il metodo a self.data.copy, perché quel metodo ritorna un vero dizionario e noi vogliamo che ritorni un'istanza della stessa classe di self.
3 Usiamo l'attributo __class__ per vedere se self è uno UserDict; nel caso, siamo fortunati perché sappiamo come copiare uno UserDict: basta creare una nuova istanza di UserDict e passarle il vero dizionario che abbiamo ricavato da self.data.
4 Se self.__class__ non è uno UserDict, allora self deve essere una sottoclasse di UserDict (come FileInfo), nel quale caso la vita si complica un po'. UserDict non sa come fare una copia di uno dei suoi discendenti; ci potrebbero, per esempio, essere altri attributi definiti nella sottoclasse, così dovremmo iterare attraverso ognuno di essi ed essere sicuri di copiarli tutti. Fortunatamente, Python viene rilasciato con un modulo che fa esattamente questo ed è chiamato copy. Non scenderò in dettaglio (per quanto si tratti di un modulo davvero interessante, se vi interessa potete esplorarlo più a fondo da soli), è sufficiente dire che copy, può copiare oggetti arbitrari Python ed è così che noi lo usiamo.
5 I rimanenti metodi sono semplici, redirezionano le chiamate ai metodi built-in di self.data.

Ulteriori letture