La vera storia delle classi con il nuovo stile

Viste dall'esterno, le classi con il nuovo stile sembrano molto simili all'implementazione originale. Comunque, le nuove classi hanno introdotto alcuni nuovi concetti:

Nei prossimi paragrafi, proverò a chiarire un po' questi concetti.

Costruttori di basso livello e __new__()

In passato le classi definivano un metodo __init__() che stabiliva il modo in cui le nuove istanze dovessero essere inizializzate dopo la loro creazione. Comunque, in alcuni casi, l'autore della classe voleva avere la possibilità di personalizzare il modo con cui le istanze venivano create - per esempio, se un oggetto avesse dovuto avere la capacità di essere ripristinato da un database persistente. Le classi di vecchio stile non hanno mai realmente previsto un escamotage per personalizzare la creazione di oggetti, nonostante ci fossero moduli di libreria che permettessero a certi tipi di oggetti di essere creati seguendo metodi alternativi (per esempio, il modulo new).

Le classi di nuovo stile hanno introdotto un nuovo metodo della classe, __new__(), che permette all'autore della classe di personalizzare il modo con cui le nuove istanze vengono create. Ridefinendo __new__(), una classe può implementare dei pattern come il Singleton Pattern, restituire un'istanza precedentemente creata (per esempio da una free list), o restituire un'istanza di una classe diversa (come una sottoclasse). Comunque, l'uso di __new__ ha altre importanti applicazioni. Per esempio, nel modulo pickle, __new__ è usato per creare istanze quando si deserializzano gli oggetti. In questo caso infatti, le istanze vengono create, ma senza invocare il metodo init.

Un altro utilizzo di __new__ lo si trova quando si eredita dai tipi immutabili. Per loro natura, questi tipi di oggetti non possono essere inizializzati con il metodo standard, ovvero __init__(). Infatti, qualunque inizializzazione speciale deve essere fatta quando l'oggetto viene creato; per esempio, se la classe desidera modificare il valore salvato dall'oggetto immutabile, il metodo __new__ può farlo passando il valore modificato al metodo __new__ della superclasse.

Descrittori

I descrittori sono una generalizzazione del concetto di metodi bound, fondamentale per l'implementazione delle classi originali. Infatti, quando un attributo di un'istanza non viene trovato nel dizionario dell'istanza, la ricerca continua con il dizionario della classe, poi con il dizionario delle superclassi, e così via in modo ricorsivo. Quando l'attributo viene trovato in un dizionario di classe (e non in quello di istanza!), l'interprete verifica che l'oggetto trovato sia una funzione oggetto Python. In tal caso, il valore restituito non è l'oggetto trovato, ma un oggetto wrapper che funge da funzione di appoggio. Quando il wrapper è chiamato, esso invoca la funzione oggetto originale dopo aver inserito l'istanza all'inizio della lista degli argomenti.

   1 def bound_f(arg):
   2     return f(x, arg)

Per esempio, considera un'istanza x di una classe C. Ora, supponiamo che ci sia una chiamata al metodo x.f(0). Questa operazione cerca l'attributo nominato f su x e lo chiama con l'argomento 0. Se f corrisponde a un metodo definito nella classe, la richiesta dell'attributo restituisce una funzione wrapper che si comporta più o meno come questa funzione in pseudocodice Python:

   1 def bound_f(arg):
   2     return f(x, arg)

Quando il wrapper è chiamato con l'argomento 0, esso invoca f con due argomenti: x e 0. Questo è il meccanismo fondamentale grazie al quale i metodi delle classi possiedono l'argomento "self".

Un altro modo per accedere alla funzione oggetto f (senza passare per il wrapper) è chiedere direttamente l'attributo f alla classe C. Questo tipo di ricerca non restituisce un wrapper, ma la funzione f. In altre parole x.f(0) è l'equivalente di C.f(x, 0). Questa è un'equivalenza abbastanza importante in Python.

Nelle classi originali, se la ricerca dell'attributo trova un altro tipo di oggetto, non viene creato il wrapper e viene restituito il valore trovato nel dizionario della classe. Questo ci permette di usare gli attributi delle classi come valori "standard" di variabili di istanza. Per esempio, nell'esempio fatto prima, se la classe C ha un attributo a il cui valore è 1 e non c'è la chiave a nel dizionario dell'istanza di x, allora x.a equivale a 1. L'assegnamento a x.a creerà una chiave a nel dizionario dell'istanza di x, il cui valore oscurerà l'attributo della classe (a causa dell'ordine di ricerca degli attributi). Rimuovendo x.a, si potrà riutilizzare il valore precedentemente nascosto (1).

Sfortunatamente, alcuni sviluppatori Python hanno scoperto le limitazioni di questo modello. Una per esempio è che essa impediva la creazione di classi "ibride" che avevano alcuni metodi implementati in Python e altri in C, poichè solo le funzioni Python venivano wrappate in modo da fornire al metodo l' accesso all'istanza, e questa funzionalità è stata inserita manualmente nel linguaggio. Non c'era neanche un modo semplice per definire diversi tipi di metodi, come funzioni membro statiche, ben note ai programmatori C++ e Java.

Per affrontare questo problema, Python 2.2 ha introdotto una straordinaria generalizzazione del comportamento del wrapper, precedentemente spiegato. Invece di inserire manualmente questa funzionalità secondo la quale le funzioni oggetto Python vengono wrappate mentre altri oggetti no, il wrapping è diventato compito dell'oggetto trovato dalla ricerca degli attributi (la funzione f nell'esempio sopra). Se l'oggetto trovato ha un metodo speciale chiamato __get__, esso viene considerato un oggetto descrittore. Il metodo __get__ viene quindi invocato e il suo valore di ritorno è usato per produrre il risultato della ricerca dell'attributo. Se l'oggetto non ha il metodo __get__, esso viene restituito senza modifiche. Per ottenere il comportamento originale (il wrapping di funzioni oggetto) senza specializzare le funzioni oggetto nel codice per la ricerca degli attributi dell'istanza, le funzioni oggetto ora hanno un metodo __get__ che restituisce un wrapper come accadeva prima. Comunque, gli utenti sono liberi di definire altre classi con il metodo __get__, e le loro istanze, quando sono trovate in un un dizionario di classe durante la ricerca di un attributo di istanza, possono effettuare un wrap tra loro in qualunque modo.

In aggiunta alla generalizzazione del concetto di ricerca degli attributi, era anche importante estendere quest'idea per le operazioni di assegnamento e eliminazione di un attributo. Quindi, uno schema simile è usato per le operazioni di assegnamento come x.a = 1 o del x.a. In questi casi, se l'attributo a è trovato nel dizionario di classe dell'istanza (non nel dizionario di istanza), si controlla se l'oggetto salvato nel dizionario di classe ha i metodi speciali __set__ e __del___. (Ricorda che __del__ ha già un significato completamente diverso.) Così, ridefinendo questi metodi, un oggetto descrittore può avere un controllo completo su ciò che vuole fare quando deve leggere, impostare, rimuovere un attributo. Comunque, è importante sottolineare che questa personalizzazione si applica solo quando un descrittore compare in un dizionario di classe - non il dizionario dell'istanza dell'oggetto.

staticmethod, classmethod, e property

Python 2.2 ha aggiunto tre classi predefinite: classmethod, staticmethod, e property, che utilizzavano il meccanismo dei descrittori. classmethod e staticmethod erano semplici wrapper per funzioni oggetto, che implementavano diversi __get__ per restituire vari tipi di wrapper per chiamare la funzione alla base. Per esempio, il wrapper staticmethod chiama la funzione senza modificare la lista degli argomenti. Il wrapper classmethod chiama la funzione con l'oggetto classe dell'istanza impostato come primo argomento al posto dell'istanza stessa. Entrambi possono essere chiamati sia con l'istanza sia con la classe e gli argomenti saranno uguali.

La classe property è un wrapper che forniva una coppia di metodi per leggere e impostare un valore in un attributo. Per esempio, se hai una classe come questa,

   1 class C(object):
   2     def set_x(self, value):
   3         self.__x = value
   4     def get_x(self):
   5         return self.__x

un wrapper property potrebbe essere usato per far sì che quando si accede all'attributo x vengano chiamati implicitamente i metodi get_x e set_x.

Quando furono introdotti, non c'era una sintassi speciale per usare i descrittori classmethod, staticmethod, e property. A quel tempo, ritenemmo che fosse troppo discutibile aggiungere nel medesimo periodo una nuova funzionalità così importante insieme con una nuova sintassi (cosa che porta sempre ad un dibattito più o meno acceso). Così, per usare queste funzioni, dovresti definire la tua classe e metodi normalmente, ma aggiungere delle istruzioni che funzionassero da wrap per i metodi. Per esempio:

   1 class C:
   2     def foo(cls, arg):
   3         ...
   4     foo = classmethod(foo)
   5     def bar(arg):
   6         ...
   7     bar = staticmethod(bar)

Per le properties fu usato uno schema simile:

   1 class C:
   2     def set_x(self, value):
   3         self.__x = value
   4     def get_x(self):
   5         return self.__x
   6     x = property(get_x, set_x)

Decoratori

Uno svantaggio di questo approccio è che chi legge il codice della classe deve leggere tutto fino a che non viene raggiunta la fine della dichiarazione del metodo prima di capire se quello è un metodo di classe o statico (o una variante definita dall'utente). In Python 2.4 è stata introdotta una nuova sintassi che permette di scrivere:

   1 class C:
   2     @classmethod
   3     def foo(cls, arg):
   4        ...
   5     @staticmethod
   6     def bar(arg):
   7          ...

Il costrutto @expression, posto una riga prima della dichiarazione della funzione, viene chiamato decoratore. (Non lo confondere con descrittore, che si riferisce al wrapper che implementa get; vedi sopra.) La scelta della particolare sintassi del decoratore (deriva dalle annotazioni di Java) fu discussa in modo piuttosto acceso fino a che non fu decisa dal voto del "BDFL". (David Beazely scrisse un articolo sulla storia del termine BDFL di cui poi vi parlerò.)

La funzione del decoratore è diventata una di quelle più di successo tra le funzioni del linguaggio, e l'uso di decoratori personalizzati ha superato le mie più ampie aspettative. Soprattutto i framew ork web hanno trovato nei decoratori molti utilizzi. Proprio per questo, in Python 2.6, la sintassi del decoratore è stata estesa dalla definizione delle funzioni per includere anche la definizione delle classi.

Slot

Un altro miglioramento reso possibile grazie ai descrittori è stata l'introduzione dell'attributo __slots__ nelle classi. Per esempio, una classe potrebbe essere definita nel seguente modo:

   1 class C:
   2     __slots__ = ['x','y']
   3     ...

La presenza di __slots__ comporta alcune conseguenze. Primo, limita l'insieme valido dei nomi degli attributi di un oggetto solo a quelli inclusi nella lista. Secondo, poichè gli attributi ora sono prefissati, non è più necessario salvare gli attributi in un'istanza di un dizionario, così l'attributo __dict__ è stato rimosso (a meno che una classe di base non ce l'abbia già; può anche essere riaggiunto da una sottoclasse che non usa __slots___). Invece, gli attributi possono essere salvati in locazioni predeterminate all'interno di un array. Così, ogni attributo slot è in realtà un oggetto descrittore che sa come ottenere/impostare ogni attributo usando gli indici dell'array. Sotto, l'implementazione di questa funzionalità è stata fatta completamente in C e ciò la rende altamente efficiente.

Alcune persone erroneamente credono che lo scopo di __slots__ sia quello di migliorare la sicurezza del codice (limitando i nomi degli attributi). In realtà, lo scopo ultimo erano le performance. Non solo __slots__ era un'interessante applicazione dei descrittori, ma ciò che temevo di più era che i cambiamenti nel sistema delle classi potesse comportare effetti negativi sulle performance. In particolare, per far funzionare correttamente i descrittori dei dati, ogni modifica agli attributi della classe prima comportava un controllo nel dizionario della classe per vedere se l'attributo fosse presente, ovvero, un descrittore dei dati. In tal caso, il descrittore era usato per gestire l'accesso agli attributi invece di far manipolare direttamente il dizionario dell'istanza come avveniva normalmente. Comunque, questo controllo extra significava una ricerca aggiuntiva prima controllare il dizionario di ogni istanza. Così l'uso di __slots__ era un modo per ottimizzare la ricerca degli attributi - un passo indietro, se vogliamo, nel caso in cui gli sviluppatori avessero trovato problemi di performance a causa del nuovo sistema delle classi. Ciò non si rivelò necessario, ma ormai era troppo tardi per rimuovere __slots__. Certo, usato appropriatamente, gli slot possono migliorare le performance, soprattutto riducendo porzioni di memoria quando oggetti molto piccoli sono creati.

Nel prossimo articolo, vedremo l'ordine di risoluzione dei metodi in Python (Method Resolution Order, MRO)

Riferimenti Esterni

Per leggere l'articolo originale in lingua inglese: The inside story of New-Style Classes


CategoryDocumentazione

StoriaDiPython/StoriaInternaNuovoStileClassi (last edited 2010-07-07 17:11:28 by Markon)