2009-03-25 14 views
98

In Python 2.5, c'è un modo per creare un decoratore che decora una classe? Nello specifico, voglio usare un decoratore per aggiungere un membro a una classe e cambiare il costruttore per prendere un valore per quel membro.Decoratore classe Python

ricerca di qualcosa di simile a quanto segue (che ha un errore di sintassi sulla 'class Foo:':

def getId(self): return self.__id 

class addID(original_class): 
    def __init__(self, id, *args, **kws): 
     self.__id = id 
     self.getId = getId 
     original_class.__init__(self, *args, **kws) 

@addID 
class Foo: 
    def __init__(self, value1): 
     self.value1 = value1 

if __name__ == '__main__': 
    foo1 = Foo(5,1) 
    print foo1.value1, foo1.getId() 
    foo2 = Foo(15,2) 
    print foo2.value1, foo2.getId() 

Grazie, Rob

Edit: Rileggendo questa domanda, indovinate un po ' Io sono davvero dopo è un modo per fare qualcosa come un'interfaccia C# in Python.Io devo cambiare il mio paradigma suppongo.

+34

Mentre è utile, i decoratori Python sono diversi da quelli del decoratore. Un nome sfortunato, suppongo. Forse finirò per andare in quella direzione. Grazie. –

+13

@Robert Gowland: molto diplomatico. –

risposta

65

Secondo l'idea che potresti voler considerare una sottoclasse anziché l'approccio che hai delineato. Tuttavia, non conoscendo il tuo scenario specifico, YMMV :-)

Quello che stai pensando è un metaclass. La funzione __new__ in un metaclass è passata alla definizione proposta completa della classe, che può quindi riscrivere prima della creazione della classe. A quel punto, puoi sostituire il costruttore con uno nuovo.

Esempio:

def substitute_init(self, id, *args, **kwargs): 
    pass 

class FooMeta(type): 

    def __new__(cls, name, bases, attrs): 
     attrs['__init__'] = substitute_init 
     return super(FooMeta, cls).__new__(cls, name, bases, attrs) 

class Foo(object): 

    __metaclass__ = FooMeta 

    def __init__(self, value1): 
     pass 

Sostituzione del costruttore è forse un po 'drammatico, ma il linguaggio non fornire il supporto per questo tipo di profonda introspezione e la modifica dinamica.

+0

Grazie, è quello che sto cercando. Una classe che può modificare qualsiasi numero di altre classi in modo che abbiano tutti un membro specifico. I motivi per cui non ho ereditato le classi da una classe ID comune sono che voglio avere versioni non ID delle classi e versioni ID. –

+0

I metaclassi erano il modo migliore per fare cose del genere in Python2.5 o precedenti, ma oggigiorno si possono usare spesso decoratori di classe (vedi la risposta di Steven), che sono molto più semplici. –

11

Non è una buona pratica e non c'è un mec hanism per farlo a causa di ciò. Il modo giusto per realizzare ciò che vuoi è ereditare.

Dai uno sguardo allo class documentation.

Una piccola premessa:

class Employee(object): 

    def __init__(self, age, sex, siblings=0): 
     self.age = age 
     self.sex = sex  
     self.siblings = siblings 

    def born_on(self):  
     today = datetime.date.today() 

     return today - datetime.timedelta(days=self.age*365) 


class Boss(Employee):  
    def __init__(self, age, sex, siblings=0, bonus=0): 
     self.bonus = bonus 
     Employee.__init__(self, age, sex, siblings) 

In questo modo Boss ha tutto Employee ha, con anche il proprio metodo __init__ e propri membri.

+3

Immagino che quello che volevo fosse avere il boss agnostico della classe che contiene. Cioè, ci possono essere dozzine di classi diverse a cui voglio applicare le funzionalità di Boss. Sono partito con queste dozzine di classi ereditate da Boss? –

+4

@Robert Gowland: Ecco perché Python ha ereditarietà multipla. Sì, dovresti ereditare vari aspetti da varie classi genitore. –

+7

@ S.Lott: In generale l'ereditarietà multipla è una cattiva idea, anche troppi livelli di ereditarietà sono anch'essi cattivi. Ti raccomanderò di stare lontano dall'ereditarietà multipla. – mpeterson

176

A parte la questione se decoratori di classe sono la giusta soluzione al vostro problema:

in Python 2.6 e superiori, ci sono decoratori di classe con la @ -syntax, in modo da poter scrivere:

@addID 
class Foo: 
    pass 

nelle versioni precedenti, si può fare in un altro modo:

class Foo: 
    pass 

Foo = addID(Foo) 

Nota tuttavia che questo funziona lo stesso che per decoratori di funzione, e che il decoratore dovrebbe r eturn la nuova (o modificata originale) classe, che non è quello che stai facendo nell'esempio. Il decoratore addID sarebbe simile a questa:

def addID(original_class): 
    orig_init = original_class.__init__ 
    # make copy of original __init__, so we can call it without recursion 

    def __init__(self, id, *args, **kws): 
     self.__id = id 
     self.getId = getId 
     orig_init(self, *args, **kws) # call the original __init__ 

    original_class.__init__ = __init__ # set the class' __init__ to the new one 
    return original_class 

È quindi possibile utilizzare la sintassi appropriata per la versione di Python come descritto sopra.

Ma concordo con gli altri sul fatto che l'ereditarietà è più adatta se si desidera eseguire l'override di __init__.

+2

... anche se la domanda menziona specificamente Python 2.5 :-) –

+0

Il tuo esempio causa un RuntimeError: massima profondità di ricorsione superata, perché quando la classe è istanziata, original_class .__ init__ è la funzione che viene chiamata, quindi chiamata __init__. Pubblicherò un esempio funzionante nel prossimo commento. –

+5

Ci scusiamo per la confusione linesep, ma esempi di codice non sono strettamente grandi nei commenti ...: def addID (original_class): original_class .__ orig__init__ = original_class .__ init__ def __init __ (self, * args, ** kw): print "decoratore" self.id = 9 original_class .__ orig__init __ (self, * args, ** KWS) original_class .__ init__ = __init__ ritorno original_class @addID class Foo: def __init __ (self): stampa "Foo" a = Foo() print a.id –

4

In realtà c'è una buona implementazione di una classe decoratrice qui:

https://github.com/agiliq/Django-parsley/blob/master/parsley/decorators.py

Io in realtà penso che questo è un'implementazione piuttosto interessante. Poiché sottoclasse la classe che decora, si comporterà esattamente come questa classe in cose come gli assegni isinstance.

Si ha un vantaggio: non è raro per l'istruzione __init__ in un modulo Django personalizzato per apportare modifiche o integrazioni self.fields quindi è meglio per le modifiche al self.fields accadano dopo tutti __init__ ha eseguito per la classe in domanda.

Molto intelligente.

Tuttavia, nella tua classe in realtà desideri che la decorazione modifichi il costruttore, che non credo sia un buon uso per un decoratore di classe.

10

nessuno ha spiegato che è possibile definire dinamicamente le classi. così si può avere un decoratore che definisce (e torna) una sottoclasse:

def addId(cls): 

    class AddId(cls): 

     def __init__(self, id, *args, **kargs): 
      super(AddId, self).__init__(*args, **kargs) 
      self.__id = id 

     def getId(self): 
      return self.__id 

    return AddId 

che può essere utilizzato in python 2 (il commento da Blckknght che spiega perché si dovrebbe continuare a fare questo in 2.6 +) in questo modo:

class Foo: 
    pass 

FooId = addId(Foo) 

e in Python 3 come questo (ma attenzione a usare super() nelle classi):

@addId 
class Foo: 
    pass 

così si può avere la botte piena e la mangiarlo - ereditarietà e decoratori!

+2

La sottoclasse in un decoratore è pericolosa in Python 2.6+, perché interrompe le chiamate 'super' nella classe originale. Se 'Foo' aveva un metodo chiamato' foo' che si chiamava 'super (Foo, self) .foo()' sarebbe retrocedere all'infinito, perché il nome 'Foo' è legato alla sottoclasse restituita dal decoratore, non alla classe originale (che non è accessibile con qualsiasi nome). Python 3's argumentless 'super()' evita questo problema (presumo tramite la stessa magia del compilatore che gli permette di funzionare a tutti). Puoi anche risolvere il problema decorando manualmente la classe con un nome diverso (come hai fatto nell'esempio di Python 2.5). – Blckknght

+1

eh. grazie, non ne avevo idea (io uso python 3). aggiungerà un commento. –

0

Sono d'accordo che l'ereditarietà si adatta meglio al problema posto.

Ho trovato questa domanda molto utile per quanto riguarda le classi di decorazione, grazie a tutti. Ecco un altro paio di esempi, sulla base di altre risposte, compreso il modo dell'eredità sulle cose in Python 2.7, (e @wraps, che mantiene docstring della funzione originaria, ecc):

def dec(klass): 
    old_foo = klass.foo 
    @wraps(klass.foo) 
    def decorated_foo(self, *args ,**kwargs): 
     print('@decorator pre %s' % msg) 
     old_foo(self, *args, **kwargs) 
     print('@decorator post %s' % msg) 
    klass.foo = decorated_foo 
    return klass 

@dec # no parentheses 
class Foo... 

Spesso si desidera aggiungere parametri al vostro decoratore:

from functools import wraps 

def dec(msg='default'): 
    def decorator(klass): 
     old_foo = klass.foo 
     @wraps(klass.foo) 
     def decorated_foo(self, *args ,**kwargs): 
      print('@decorator pre %s' % msg) 
      old_foo(self, *args, **kwargs) 
      print('@decorator post %s' % msg) 
     klass.foo = decorated_foo 
     return klass 
    return decorator 

@dec('foo decorator') # you must add parentheses now, even if they're empty 
class Foo(object): 
    def foo(self, *args, **kwargs): 
     print('foo.foo()') 

@dec('subfoo decorator') 
class SubFoo(Foo): 
    def foo(self, *args, **kwargs): 
     print('subfoo.foo() pre') 
     super(SubFoo, self).foo(*args, **kwargs) 
     print('subfoo.foo() post') 

@dec('subsubfoo decorator') 
class SubSubFoo(SubFoo): 
    def foo(self, *args, **kwargs): 
     print('subsubfoo.foo() pre') 
     super(SubSubFoo, self).foo(*args, **kwargs) 
     print('subsubfoo.foo() post') 

SubSubFoo().foo() 

Uscite:

@decorator pre subsubfoo decorator 
subsubfoo.foo() pre 
@decorator pre subfoo decorator 
subfoo.foo() pre 
@decorator pre foo decorator 
foo.foo() 
@decorator post foo decorator 
subfoo.foo() post 
@decorator post subfoo decorator 
subsubfoo.foo() post 
@decorator post subsubfoo decorator 

ho usato una funzione di decoratore, come li trovo più conciso. Ecco una classe per decorare una classe:

class Dec(object): 

    def __init__(self, msg): 
     self.msg = msg 

    def __call__(self, klass): 
     old_foo = klass.foo 
     msg = self.msg 
     def decorated_foo(self, *args, **kwargs): 
      print('@decorator pre %s' % msg) 
      old_foo(self, *args, **kwargs) 
      print('@decorator post %s' % msg) 
     klass.foo = decorated_foo 
     return klass 

Una versione più robusta che verifica la presenza di quelle parentesi, e funziona se non esistono i metodi della classe decorato:

from inspect import isclass 

def decorate_if(condition, decorator): 
    return decorator if condition else lambda x: x 

def dec(msg): 
    # Only use if your decorator's first parameter is never a class 
    assert not isclass(msg) 

    def decorator(klass): 
     old_foo = getattr(klass, 'foo', None) 

     @decorate_if(old_foo, wraps(klass.foo)) 
     def decorated_foo(self, *args ,**kwargs): 
      print('@decorator pre %s' % msg) 
      if callable(old_foo): 
       old_foo(self, *args, **kwargs) 
      print('@decorator post %s' % msg) 

     klass.foo = decorated_foo 
     return klass 

    return decorator 

Le assert verifica che il decoratore non è stato usato senza parentesi.In tal caso, la classe che viene decorata viene passata al parametro msg del decoratore, che genera un valore AssertionError.

@decorate_if applica solo il decorator se condition restituisce True.

Il getattr, callable prova, e @decorate_if vengono utilizzati in modo che il decoratore non si rompe se il metodo foo() non esiste sulla classe da decorare.