Comment décorer une classe?

129

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 changer le constructeur pour prendre 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 __name__ == '__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 dois changer de paradigme je suppose.

Robert Gowland
la source

Réponses:

80

J'appuie l'idée que vous voudrez peut-être envisager une sous-classe au lieu de l'approche que vous avez décrite. Cependant, ne connaissant pas votre scénario spécifique, YMMV :-)

Ce à quoi vous pensez est une métaclasse. La __new__fonction d'une métaclasse reçoit la définition complète proposée de 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

Le remplacement du constructeur est peut-être un peu dramatique, mais le langage fournit un support pour ce genre d'introspection profonde et de modification dynamique.

Jarret Hardie
la source
Merci, c'est ce que je recherche. Une classe qui peut modifier n'importe quel nombre d'autres classes de sorte qu'elles aient toutes un membre particulier. Les raisons pour lesquelles les classes n'héritent pas d'une classe ID commune sont que je souhaite avoir des versions non ID des classes ainsi que des versions ID.
Robert Gowland
Les métaclasses étaient autrefois le moyen idéal pour faire des choses comme celle-ci dans Python2.5 ou plus ancien, mais de nos jours, vous pouvez très souvent utiliser des décorateurs de classe (voir la réponse de Steven), qui sont beaucoup plus simples.
Jonathan Hartley
203

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 avec @ -syntax, vous pouvez donc é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 l'original modifié), 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 est mieux adapté si vous voulez passer outre __init__.

Steven
la source
2
... même si la question mentionne spécifiquement Python 2.5 :-)
Jarret Hardie
5
Désolé pour le désordre de ligne, mais les exemples de code ne sont pas strictement bons dans les commentaires ...: def addID (original_class): original_class .__ orig__init__ = original_class .__ init__ def init __ (self, * args, ** kws): print "decorator" self.id = 9 original_class .__ orig__init __ (self, * args, ** kws) original_class .__ init = init return original_class @addID class Foo: def __init __ (self): print "Foo" a = Foo () print a.id
Gerald Senarclens de Grancy
2
@Steven, @Gerald a raison, si vous pouviez mettre à jour votre réponse, il serait beaucoup plus facile de lire son code;)
Jour
3
@Day: merci pour le rappel, je n'avais pas remarqué les commentaires avant. Cependant: j'obtiens également une profondeur de récursivité maximale dépassée avec le code Geralds. Je vais essayer de faire une version de travail ;-)
Steven
1
@Day et @Gerald: désolé, le problème n'était pas avec le code de Gerald, je me suis trompé à cause du code mutilé dans le commentaire ;-)
Steven
20

Personne n'a expliqué que vous pouvez définir dynamiquement 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

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

class Foo:
    pass

FooId = addId(Foo)

Et en Python 3 comme ça (mais 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!

Andrew Cooke
la source
4
Le sous-classement dans un décorateur est dangereux dans Python 2.6+, car il interrompt les superappels dans la classe d'origine. S'il y Fooavait une méthode nommée fooqui l'appelait, super(Foo, self).foo()elle se répéterait à l'infini, car le nom Fooest lié à la sous-classe retournée par le décorateur, et non à la classe d'origine (qui n'est accessible par aucun nom). Le sans argument de Python 3 super()évite ce problème (je suppose que via la même magie du compilateur qui lui permet de fonctionner du tout). Vous pouvez également contourner le problème en décorant manuellement la classe sous un nom différent (comme vous l'avez fait dans l'exemple Python 2.5).
Blckknght
1
hein. merci, je n'avais aucune idée (j'utilise python 3). ajoutera un commentaire.
andrew cooke
13

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

Jetez un œil à la 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 Employee, avec aussi sa propre __init__méthode et ses propres membres.

mpeterson
la source
3
Je suppose que ce que je voulais, c'était avoir un Boss indépendant de la classe qu'il contient. Autrement dit, il peut y avoir des dizaines de classes différentes auxquelles je souhaite appliquer les fonctionnalités de Boss. Me reste-t-il avec ces douzaines de classes héritées de Boss?
Robert Gowland
5
@Robert Gowland: C'est pourquoi Python a un héritage multiple. Oui, vous devez hériter de divers aspects de diverses classes parentes.
S.Lott
7
@ S.Lott: En général, l'héritage multiple est une mauvaise idée, même trop de niveaux d'héritage l'est également. Je vous recommanderai de rester à l'écart de l'héritage multiple.
mpeterson
5
mpeterson: L'héritage multiple en python est-il pire que cette approche? Quel est le problème avec l'héritage multiple de python?
Arafangion
2
@Arafangion: L'héritage multiple est généralement considéré comme un signe d'alerte de problèmes. Cela crée des hiérarchies complexes et des relations difficiles à suivre. Si votre domaine de problème se prête bien à l'héritage multiple, (peut-il être modélisé hiérarchiquement?), C'est un choix naturel pour beaucoup. Cela s'applique à toutes les langues qui autorisent l'héritage multiple.
Morten Jensen
6

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 quelques exemples supplémentaires , basés sur d'autres réponses, y compris la façon dont l'héritage affecte les choses dans Python 2.7, (et @wraps , qui maintient 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...

Vous souhaitez souvent 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 ces parenthèses et fonctionne si les méthodes n'existent pas sur 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

Les assertvérifications 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 passée au msgparamètre du décorateur, ce qui déclenche un AssertionError.

@decorate_ifapplique uniquement le decoratorif conditionà True.

Les getattr, callabletest et @decorate_ifsont utilisés pour que le décorateur ne s'arrête pas si la foo()méthode n'existe pas sur la classe en cours de décoration.

Chris
la source
4

Il y a en fait une assez 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 mise en œuvre assez intéressante. Comme il sous-classe la classe qu'il décore, il se comportera exactement comme cette classe dans des choses comme les isinstancecontrôles.

Cela a un avantage supplémentaire: il n'est pas rare que l' __init__instruction dans un formulaire django personnalisé apporte des modifications ou des ajouts self.fields, il est donc préférable que les modifications self.fieldsse produisent après que tout __init__a été exécuté pour la classe en question.

Très intelligent.

Cependant, dans votre classe, vous voulez en fait que la décoration modifie le constructeur, ce que je ne pense pas être un bon cas d'utilisation pour un décorateur de classe.

Jordan Reiter
la source
0

Voici un exemple qui répond à la question du retour des paramètres d'une classe. De plus, il respecte toujours la chaîne d'héritage, c'est-à-dire que seuls les paramètres de la classe elle-même sont retournés. La fonction get_paramsest ajoutée à titre d'exemple simple, mais d'autres fonctionnalités peuvent être ajoutées grâce au module 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']
Patricio Astudillo
la source