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.
|
Nella IDE di Python su Windows, potete aprire
rapidamente qualunque modulo nel vostro library path usando
-> (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:
def __init__(self, dict=None):
self.data = {}
if dict is not None: self.update(dict)
|
Notate che UserDict è una classe base,
non eredita da nessun'altra classe.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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()
def copy(self):
if self.__class__ is UserDict:
return UserDict(self.data)
import copy
return copy.copy(self)
def keys(self): return self.data.keys()
def items(self): return self.data.items()
def values(self): return self.data.values()
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
I rimanenti metodi sono semplici, redirezionano le chiamate
ai metodi built-in di self.data.
|