web-dev-qa-db-fra.com

Comment décorer une classe?

Dans Python 2.5, existe-t-il un moyen de créer un décorateur qui décore une classe? Plus précisément, je souhaite utiliser un décorateur pour ajouter un membre à une classe et modifier le constructeur en prenant une valeur pour ce membre.

Vous recherchez quelque chose comme ce qui suit (qui a une erreur de syntaxe sur '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 __== '__main__':
    foo1 = Foo(5,1)
    print foo1.value1, foo1.getId()
    foo2 = Foo(15,2)
    print foo2.value1, foo2.getId()

Je suppose que ce que je recherche vraiment, c'est un moyen de faire quelque chose comme une interface C # en Python. Je suppose que je dois changer de paradigme.

116
Robert Gowland

Deuxièmement, vous voudrez peut-être envisager une sous-classe plutôt que l’approche que vous avez décrite. Cependant, ne connaissant pas votre scénario spécifique, YMMV :-)

Vous pensez à une métaclasse. Le __new__ fonction dans une métaclasse reçoit la définition complète proposée pour la classe, qu’elle peut ensuite réécrire avant la création de la classe. Vous pouvez, à ce moment-là, remplacer le constructeur par un nouveau.

Exemple:

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

Remplacer le constructeur est peut-être un peu spectaculaire, mais le langage fournit un support pour ce type d'introspection profonde et de modification dynamique.

72
Jarret Hardie

Outre la question de savoir si les décorateurs de classe sont la bonne solution à votre problème:

Dans Python 2.6 et supérieur, il existe des décorateurs de classe dotés de la syntaxe @, afin que vous puissiez écrire:

@addID
class Foo:
    pass

Dans les anciennes versions, vous pouvez le faire d'une autre manière:

class Foo:
    pass

Foo = addID(Foo)

Notez cependant que cela fonctionne de la même manière que pour les décorateurs de fonctions et que le décorateur doit renvoyer la nouvelle classe (ou la classe d'origine modifiée), ce qui n'est pas ce que vous faites dans l'exemple. Le décorateur addID ressemblerait à ceci:

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

Vous pouvez ensuite utiliser la syntaxe appropriée pour votre version Python comme décrit ci-dessus).

Mais je suis d’accord avec d’autres pour dire que l’héritage convient mieux si vous souhaitez remplacer __init__.

192
Steven

Personne n'a expliqué que vous pouvez définir de manière dynamique des classes. Vous pouvez donc avoir un décorateur qui définit (et retourne) une sous-classe:

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

Ce qui peut être utilisé dans Python 2 (le commentaire de Blckknght qui explique pourquoi vous devriez continuer à le faire dans la version 2.6+)) comme ceci:

class Foo:
    pass

FooId = addId(Foo)

Et dans Python 3 comme ceci (mais faites attention à utiliser super() dans vos classes)):

@addId
class Foo:
    pass

Ainsi, vous pouvez avoir votre gâteau et le manger - héritage et décorateurs!

15
andrew cooke

Ce n'est pas une bonne pratique et il n'y a pas de mécanisme pour le faire à cause de cela. La bonne façon d'accomplir ce que vous voulez, c'est l'héritage.

Jetez un coup d'oeil dans le documentation de la classe .

Un petit exemple:

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)

De cette façon, Boss a tout ce que Employee a, ainsi que sa propre méthode __init__ et ses propres membres.

12
mpeterson

Il y a en fait une très bonne implémentation d'un décorateur de classe ici:

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

Je pense en fait que c'est une implémentation assez intéressante. Comme il classe la classe qu'il décore, il se comportera exactement comme cette classe dans des choses comme isinstance chèques.

Cela présente un avantage supplémentaire: il n’est pas rare que l’instruction __init__ Dans une coutume Django Form pour apporter des modifications ou des ajouts à self.fields, Il est donc préférable de modifier self.fields À arriver après tout le __init__ A été exécuté pour la classe en question.

Très intelligent.

Cependant, dans votre classe, vous souhaitez réellement que la décoration modifie le constructeur, ce qui, à mon avis, n’est pas un bon cas d’utilisation pour un décorateur de classe.

4
Jordan Reiter

Je conviens que l'héritage convient mieux au problème posé.

J'ai trouvé cette question très pratique sur les cours de décoration, merci à tous.

Voici deux autres exemples, basés sur d'autres réponses, y compris l'impact de l'héritage sur Python 2.7, (et @ wraps) , qui conserve la docstring de la fonction d'origine, etc. ):

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...

Souvent, vous voulez ajouter des paramètres à votre décorateur:

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()

Les sorties:

@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

J'ai utilisé un décorateur de fonction, car je les trouve plus concis. Voici une classe pour décorer une 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

Une version plus robuste qui vérifie la présence de ces parenthèses et fonctionne si les méthodes n'existent pas dans la classe décorée:

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 vérifie que le décorateur n'a pas été utilisé sans parenthèses. Si tel est le cas, la classe en cours de décoration est transmise au paramètre msg du décorateur, ce qui déclenche un AssertionError.

@decorate_if Applique uniquement le decorator si condition est évalué à True.

Les tests getattr, callable et @decorate_if Sont utilisés pour que le décorateur ne s'interrompe pas si la méthode foo() n'existe pas dans la classe. être décoré.

3
Chris