180 votes

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 veux utiliser un décorateur pour ajouter un membre à une classe et modifier le constructeur pour qu'il prenne une valeur pour ce membre.

Je cherche quelque chose comme ce qui suit (qui présente 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 crois que ce que je cherche vraiment, c'est un moyen de faire quelque chose comme une interface C# en Python. Je dois changer de paradigme, je suppose.

244voto

Steven Points 10243

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

Dans Python 2.6 et plus, il existe des décorateurs de classe avec la syntaxe @, ce qui vous permet d'écrire :

@addID
class Foo:
    pass

Dans les versions plus anciennes, vous pouvez procéder 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 retourner la nouvelle classe (ou la classe originale 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 alors utiliser la syntaxe appropriée pour votre version de Python, comme décrit ci-dessus.

Mais je suis d'accord avec d'autres que l'héritage est mieux adapté si vous voulez remplacer __init__ .

2 votes

... même si la question mentionne spécifiquement Python 2.5 :-)

0 votes

Votre exemple provoque une RuntimeError : maximum recursion depth exceeded, car lorsque la classe est instanciée, original_class.__init__ est la fonction qui est appelée, ayant donc init l'appel lui-même. Je posterai un exemple fonctionnel dans le prochain commentaire.

5 votes

Désolé pour le désordre linesep, mais les échantillons de code ne sont pas vraiment super 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

87voto

Jarret Hardie Points 36266

Je soutiens l'idée que vous pourriez 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. Le site __new__ dans une métaclasse se voit transmettre la définition complète proposée pour la classe, qu'elle peut alors 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 dramatique, mais le langage fournit un support pour ce type d'introspection profonde et de modification dynamique.

1 votes

Merci, c'est ce que je cherche. Une classe qui peut modifier un nombre quelconque d'autres classes de façon à ce qu'elles aient toutes un membre particulier. Si les classes n'héritent pas d'une classe ID commune, c'est parce que je veux avoir des versions non ID des classes ainsi que des versions ID.

3 votes

Les métaclasses étaient le moyen le plus courant de faire ce genre de choses dans Python2.5 ou plus ancien, mais aujourd'hui, vous pouvez très souvent utiliser les décorateurs de classe (voir la réponse de Steven), qui sont beaucoup plus simples.

35voto

andrew cooke Points 20902

Personne n'a expliqué que l'on pouvait définir dynamiquement les classes. Vous pouvez donc avoir un décorateur qui définit (et renvoie) 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 à le faire dans 2.6+) comme ceci :

class Foo:
    pass

FooId = addId(Foo)

Et en Python 3, comme ceci (mais attention à l'utilisation de super() dans vos classes) :

@addId
class Foo:
    pass

Donc vous pouvez avoir votre gâteau et mangez-le - héritage et les décorateurs !

5 votes

La sous-classification dans un décorateur est dangereuse dans Python 2.6+, parce qu'elle brise super appels dans la classe d'origine. Si Foo avait une méthode nommée foo qui a appelé super(Foo, self).foo() la récursivité serait infinie, car le nom Foo est lié à la sous-classe renvoyée par le décorateur, et non à la classe originale (qui n'est accessible sous aucun nom). La fonction sans argument de Python 3 super() évite ce problème (je suppose que c'est la même magie du compilateur qui lui permet de fonctionner). 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 de Python 2.5).

1 votes

Merci, je n'avais aucune idée (j'utilise python 3). j'ajouterai un commentaire.

13voto

mpeterson Points 551

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 est l'héritage.

Jetez un coup d'œil à la documentation des classes .

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, le patron a tout Employee a, avec aussi son propre __init__ et ses propres membres.

4 votes

Je pense que ce que je voulais, c'était que le patron soit agnostique par rapport à la classe qu'il contient. En d'autres termes, il peut y avoir des dizaines de classes différentes auxquelles je veux appliquer les fonctionnalités de Boss. Est-ce que je dois me contenter de faire en sorte que ces dizaines de classes héritent de Boss ?

5 votes

@Robert Gowland : C'est pourquoi Python a l'héritage multiple. Oui, vous devriez hériter de divers aspects de diverses classes parentes.

7 votes

@S.Lott : En général, l'héritage multiple est une mauvaise idée, même trop de niveaux d'héritage est mauvais aussi. Je vous recommande de rester à l'écart de l'héritage multiple.

6voto

Jordan Reiter Points 8679

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

En fait, je pense que c'est une mise en œuvre assez intéressante. Parce qu'elle sous-classe la classe qu'elle décore, elle se comportera exactement comme cette classe pour des choses telles que isinstance des contrôles.

Il y a un avantage supplémentaire : il n'est pas rare que la __init__ dans un formulaire personnalisé de django pour apporter des modifications ou des ajouts à self.fields Il est donc préférable que les changements self.fields à se produire après tous les __init__ a couru pour la classe en question.

Très astucieux.

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X