Come decorare una classe?


129

In Python 2.5, c'è un modo per creare un decoratore che decora una classe? In particolare, desidero utilizzare un decoratore per aggiungere un membro a una classe e modificare il costruttore per ottenere un valore per quel membro.

Cerchi qualcosa di simile al seguente (che presenta un errore di sintassi su '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()

Immagino che quello che sto veramente cercando sia un modo per fare qualcosa come un'interfaccia C # in Python. Devo cambiare il mio paradigma suppongo.

Risposte:


80

Vorrei secondare l'idea che potresti voler considerare una sottoclasse invece dell'approccio che hai delineato. Tuttavia, non conoscendo il tuo scenario specifico, YMMV :-)

Quello a cui stai pensando è una metaclasse. Alla __new__funzione in una metaclasse viene passata la definizione proposta completa della classe, che può quindi riscrivere prima della creazione della classe. È possibile, in quel momento, sostituire il costruttore per 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

Sostituire il costruttore è forse un po 'drammatico, ma il linguaggio fornisce supporto per questo tipo di profonda introspezione e modifica dinamica.


Grazie, è quello che sto cercando. Una classe che può modificare un numero qualsiasi di altre classi in modo tale che tutte abbiano un determinato membro. Le mie ragioni per non avere le classi ereditate da una classe ID comune è che voglio avere versioni non ID delle classi e versioni ID.
Robert Gowland,

I metaclassi erano il modo migliore per fare cose del genere in Python2.5 o precedenti, ma al giorno d'oggi puoi usare molto spesso decoratori di classe (vedi la risposta di Steven), che sono molto più semplici.
Jonathan Hartley,

203

A parte la domanda se i decoratori di classe siano la giusta soluzione al tuo problema:

In Python 2.6 e versioni successive, ci sono decoratori di classe con @ -syntax, quindi puoi scrivere:

@addID
class Foo:
    pass

Nelle versioni precedenti, puoi farlo in un altro modo:

class Foo:
    pass

Foo = addID(Foo)

Si noti tuttavia che questo funziona come per i decoratori di funzioni e che il decoratore dovrebbe restituire la nuova classe (o originale modificato), che non è ciò che si sta facendo nell'esempio. Il decoratore addID sarebbe simile al seguente:

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 sono d'accordo con gli altri sul fatto che l'ereditarietà sia più adatta se si desidera scavalcare __init__.


2
... anche se la domanda menziona specificamente Python 2.5 :-)
Jarret Hardie il

5
Ci scusiamo per il disordine di linesep, ma i campioni di codice non sono strettamente validi nei commenti ...: def addID (original_class): original_class .__ orig__init__ = original_class .__ init__ def init __ (self, * args, ** kws): stampa "decoratore" self.id = 9 original_class .__ orig__init __ (self, * args, ** kws) original_class .__ init = init return original_class @addID class Foo: def __init __ (self): stampa "Foo" a = Foo () stampa a.id
Gerald Senarclens de Grancy,

2
@Steven, @Gerald ha ragione, se potessi aggiornare la tua risposta, sarebbe molto più facile leggere il suo codice;)
Giorno

3
@Giorno: grazie per il promemoria, non avevo notato i commenti prima. Tuttavia: ottengo anche la massima profondità di ricorsione superata con il codice Geralds. Proverò a fare una versione funzionante ;-)
Steven

1
@Day e @Gerald: scusa, il problema non era con il codice di Gerald, ho sbagliato a causa del codice maledetto nel commento ;-)
Steven

20

Nessuno ha spiegato che puoi definire dinamicamente le classi. Quindi puoi avere un decoratore che definisce (e restituisce) 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 usato in Python 2 (il commento di Blckknght che spiega perché dovresti continuare a farlo in 2.6+) in questo modo:

class Foo:
    pass

FooId = addId(Foo)

E in Python 3 in questo modo (ma fai attenzione a usare super()nelle tue classi):

@addId
class Foo:
    pass

Quindi puoi avere la tua torta e mangiarla - eredità e decoratori!


4
La sottoclasse in un decoratore è pericolosa in Python 2.6+, perché interrompe le superchiamate nella classe originale. Se Fooavesse un metodo chiamato fooche lo chiamava super(Foo, self).foo(), si ricorrerebbe all'infinito, perché il nome Fooè associato alla sottoclasse restituita dal decoratore, non alla classe originale (che non è accessibile con nessun nome). L'irresistibile di Python 3 super()evita questo problema (presumo tramite la stessa magia del compilatore che gli consente di funzionare). Puoi anche aggirare 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 (uso python 3). aggiungerà un commento.
Andrew Cooke,

13

Questa non è una buona pratica e non esiste alcun meccanismo per farlo a causa di ciò. Il modo giusto per ottenere ciò che vuoi è l'eredità.

Dai un'occhiata alla documentazione della lezione .

Un piccolo esempio:

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 ciò che Employeeha, con il suo __init__metodo e i suoi membri.


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

5
@Robert Gowland: ecco perché Python ha eredità multipla. Sì, dovresti ereditare vari aspetti da varie classi genitore.
S.Lott

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

5
mpeterson: L'ereditarietà multipla in Python è peggiore di questo approccio? Cosa c'è di sbagliato nell'eredità multipla di Python?
Arafangion,

2
@Arafangion: l'ereditarietà multipla è generalmente considerata un segnale di avvertimento di problemi. Crea gerarchie complesse e relazioni difficili da seguire. Se il tuo dominio problematico si presta bene alla multi ereditarietà (può essere modellato gerarchicamente?) È una scelta naturale per molti. Questo vale per tutte le lingue che consentono l'ereditarietà multipla.
Morten Jensen,

6

Concordo che l'eredità si adatta meglio al problema posto.

Ho trovato questa domanda molto utile anche se sui corsi di decorazione, grazie a tutti.

Ecco un altro paio di esempi, basati su altre risposte, incluso il modo in cui l'ereditarietà influisce sulle cose in Python 2.7, (e @wraps , che mantiene il dotstring della funzione originale, 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 vuoi aggiungere parametri al tuo 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 un decoratore di funzioni, poiché li trovo più concisi. Ecco una lezione per decorare una lezione:

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 controlla quelle parentesi e funziona se i metodi non esistono sulla classe decorata:

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

I assertcontrolli che il decoratore non è stata usata senza parentesi. In tal caso, la classe che viene decorata viene passata al msgparametro del decoratore, che genera un AssertionError.

@decorate_ifapplica solo decoratorse conditionvaluta a True.

I getattr, callabletest, e @decorate_ifvengono utilizzati in modo che il decoratore non si rompa se il foo()metodo non esiste sulla classe che viene decorata.


4

In realtà c'è un'implementazione abbastanza buona di un decoratore di classe qui:

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

In realtà penso che questa sia un'implementazione piuttosto interessante. Poiché subclasse la classe che decora, si comporterà esattamente come questa classe in cose come i isinstancecontrolli.

Ha un ulteriore vantaggio: non è insolito che l' __init__istruzione in un modulo django personalizzato apporti modifiche o aggiunte, self.fieldsquindi è meglio che le modifiche self.fieldsavvengano dopo che tutto __init__è stato eseguito per la classe in questione.

Molto intelligente.

Tuttavia, nella tua classe vuoi davvero che la decorazione cambi il costruttore, che non credo sia un buon caso per un decoratore di classe.


0

Ecco un esempio che risponde alla domanda di restituzione dei parametri di una classe. Inoltre, rispetta ancora la catena dell'ereditarietà, ovvero vengono restituiti solo i parametri della classe stessa. La funzione get_paramsviene aggiunta come semplice esempio, ma è possibile aggiungere altre funzionalità grazie al modulo inspect.

import inspect 

class Parent:
    @classmethod
    def get_params(my_class):
        return list(inspect.signature(my_class).parameters.keys())

class OtherParent:
    def __init__(self, a, b, c):
        pass

class Child(Parent, OtherParent):
    def __init__(self, x, y, z):
        pass

print(Child.get_params())
>>['x', 'y', 'z']

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.