4.6. Metodi speciali per le classi

In aggiunta ai normali metodi delle classi, c'è un certo numero di metodi speciali che le classi Python possono definire. Invece di essere chiamati direttamente dal vostro codice (come metodi normali), i metodi speciali sono chiamati per voi da Python in particolari circostanze o quando viene adoperata una certa sintassi.

Come avete visto nella sezione precedente, i metodi normali vanno ben oltre il wrapping di un dizionario in una classe. Ma i metodi normali da soli non sono abbastanza perché ci sono una sacco di cose che potete fare con i dizionari oltre a chiamare i loro metodi. Per i novizi, potete ottenere ed impostare elementi con una sintassi che non prevede l'esplicita invocazione di un metodo. È qui che saltano fuori i metodi speciali di una classe: mettono a disposizione un modo per mappare la sintassi senza-chiamata-metodo in una vera chiamata a metodo.

Esempio 4.13. Il metodo speciale __getitem__

    def __getitem__(self, key): return self.data[key]
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3")
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__getitem__("name") 1
'/music/_singles/kairo.mp3'
>>> f["name"]             2
'/music/_singles/kairo.mp3'
1 Il metodo speciale __getitem__ sembra essere abbastanza semplice. Come i metodi clear, keys e values, semplicemente redireziona la chiamata al dizionario per ritornare il suo valore. Ma come viene chiamato? Beh, potete chiamare __getitem__ direttamente, ma in pratica non lo farete; io lo faccio giusto per mostrarvi come funziona. Il modo giusto di usare __getitem__ è di farlo chiamare per voi da Python.
2 Questa sembra proprio la sintassi che usereste per ottenere un valore da un dizionario ed infatti restituisce il valore che vi aspettereste. Ma qui si vede l'anello mancante: sotto sotto, Python ha convertito questa sintassi in una chiamata a metodo f.__getitem__("name"). Ecco perché __getitem__ è un metodo speciale; non solo lo potete chiamare esplicitamente ma potete fare in modo che Python lo chiami per voi, utilizzando la sintassi corretta.

Esempio 4.14. Il metodo speciale __setitem__

    def __setitem__(self, key, item): self.data[key] = item
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__setitem__("genre", 31) 1
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':31}
>>> f["genre"] = 32            2
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':32}
1 Come il metodo __getitem__, __setitem__ per fare il suo lavoro semplicemente redireziona la chiamata al vero dizionario self.data e come per __getitem__ non lo chiamerete in maniera diretta come qui; Python chiama __setitem__ per voi quando usate la sintassi corretta.
2 Questa somiglia alla sintassi regolare dei dizionari, eccetto naturalmente per il fatto che f è una classe che cerca di travestirsi da dizionario e __setitem__ è una parte essenziale del suo travestimento. Questa riga di codice va. sotto sotto, a chiamare f.__setitem__("genre", 32).

__setitem__ è un metodo speciale di classe perché viene chiamato per voi, ma è comunque un metodo della classe. Tanto semplicemente quanto il metodo __setitem__ è stato definito in UserDict, noi lo possiamo ridefinire nelle nostre classi discendenti per sovrascrivere il metodo antenato. Questo ci permette di definire classi che agiscono come dizionari in alcuni casi, ma definirne il loro comportamento ben oltre quello dei dizionari built-in.

Questo concetto sta alla base dell'intero framework che stiamo studiando in questo capitolo. Ogni tipo di file può avere una classe handler che sa come ottenere i metadati da un particolare tipo di file. Una volta che alcuni attributi (come il nome del file e la sua locazione) sono noti, la classe handler sa come derivare altri attributi automaticamente. Questo è fatto sovrascrivendo il metodo __setitem__, controllando particolari chiavi ed elaborandole nel momento in cui vengono trovate.

Per esempio, MP3FileInfo è un discendente di FileInfo. Quando il nome di un MP3FileInfo è impostato, non si limita ad impostare la chiave name (come fa l'antenato FileInfo); va anche a cercare tag MP3 nel file e popola l'intero insieme di chiavi.

Esempio 4.15. Sovrascrivere __setitem__ in MP3FileInfo

    def __setitem__(self, key, item):         1
        if key == "name" and item:            2
            self.__parse(item)                3
        FileInfo.__setitem__(self, key, item) 4
1 Notate che il nostro metodo __setitem__ è definito nello stesso modo in cui era definito l'antenato. Questo è importante, in quanto Python dovrà chiamare il metodo per noi e si aspetta che vi siano definiti un certo numero di argomenti (parlando tecnicamente, i nomi degli argomenti non contano, ha importanza solamente il loro numero).
2 Questo è il punto cruciale dell'intera classe MP3FileInfo: se stiamo assegnando un valore alla chiave name, vogliamo fare qualcosa in più.
3 L'elaborazione aggiuntiva che vogliamo fare per i nomi è incapsulata nel metodo __parse. Si tratta di un altro metodo della classe definito in MP3FileInfo e quando lo chiamiamo, lo qualifichiamo con self. La semplice chiamata a __parse sembrerebbe l'invocazione di una funzione definita fuori dalla classe, che non è ciò che noi vogliamo; chiamando self.__parse sembrerà la chiamata ad un metodo definito nella classe. Non si tratta di nulla di nuovo; referenziate gli attributi nella medesima maniera.
4 Dopo aver effettuato la nostra elaborazione aggiuntiva, vogliamo chiamare il metodo dell'antenato. Ricordate, questo non viene mai fatto per voi da Python; lo dovete fare manualmente. Notate che stiamo chiamando l'antenato immediatamente precedente, FileInfo, per quanto non abbia un metodo __setitem__. Va bene, perché Python seguirà l'albero degli antenati finché non trova una classe con il metodo che stiamo chiamando, così questa riga di codice, eventualmente, troverà e chiamerà il metodo __setitem__, definito in UserDict.
Nota
Quando accedete agli attributi di una classe, avete bisogno di qualificare il nome dell'attributo: self.attributo. Quando chiamate altri metodi di una classe, dovete qualificare il nome del metodo: self.metodo.

Esempio 4.16. Impostare il nome di un MP3FileInfo

>>> import fileinfo
>>> mp3file = fileinfo.MP3FileInfo()                   1
>>> mp3file
{'name':None}
>>> mp3file["name"] = "/music/_singles/kairo.mp3"      2
>>> mp3file
{'album': 'Rave Mix', 'artist': '***DJ MARY-JANE***', 'genre': 31,
'title': 'KAIRO****THE BEST GOA', 'name': '/music/_singles/kairo.mp3',
'year': '2000', 'comment': 'http://mp3.com/DJMARYJANE'}
>>> mp3file["name"] = "/music/_singles/sidewinder.mp3" 3
>>> mp3file
{'album': '', 'artist': 'The Cynic Project', 'genre': 18, 'title': 'Sidewinder',
'name': '/music/_singles/sidewinder.mp3', 'year': '2000',
'comment': 'http://mp3.com/cynicproject'}
1 Prima, creiamo un'istanza di MP3FileInfo, senza passarle alcun nome di file (possiamo evitarlo in quanto l'argomento filename del metodo __init__ è opzionale). Siccome MP3FileInfo non ha un suo metodo __init__, Python percorre l'albero degli antenati e trova il metodo __init__ di FileInfo. Questo metodo __init__ chiama manualmente il metodo __init__ di UserDict e quindi imposta la chiave name al valore di filename, che è None, in quanto non abbiamo passato l'argomento filename. Dunque mp3file inizialmente sembra un dizionario con una chiave, name, il cui valore è None.
2 Ora inizia il vero divertimento. Impostando la chiave name di mp3file viene invocato il metodo __setitem__ di MP3FileInfo (non di UserDict), il quale nota che stiamo impostando la chiave name con un vero valore e chiama self.__parse. Anche se non abbiamo ancora analizzato a fondo il metodo __parse, potete vedere dall'output che va ad impostare altre chiavi: album, artist, genre, title, year, e comment.
3 Modificare la chiave name innescherà nuovamente lo stesso processo: Python chiama __setitem__, che chiama self.__parse, che imposta tutte le altre chiavi.