Wie dekoriere ich eine Klasse?

128

Gibt es in Python 2.5 eine Möglichkeit, einen Dekorator zu erstellen, der eine Klasse dekoriert? Insbesondere möchte ich einen Dekorator verwenden, um ein Element zu einer Klasse hinzuzufügen und den Konstruktor so zu ändern, dass er einen Wert für dieses Mitglied annimmt.

Suchen Sie nach etwas wie dem folgenden (das einen Syntaxfehler in 'class Foo:' hat:):

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

Ich denke, was ich wirklich will, ist eine Möglichkeit, so etwas wie eine C # -Schnittstelle in Python zu machen. Ich muss wohl mein Paradigma wechseln.

Robert Gowland
quelle

Antworten:

80

Ich würde den Gedanken unterstützen, dass Sie möglicherweise eine Unterklasse anstelle des von Ihnen skizzierten Ansatzes in Betracht ziehen möchten. Allerdings nicht bekannt, Ihr spezifisches Szenario, YMMV :-)

Was Sie denken, ist eine Metaklasse. Der __new__Funktion in einer Metaklasse wird die vollständige vorgeschlagene Definition der Klasse übergeben, die sie dann neu schreiben kann, bevor die Klasse erstellt wird. Zu diesem Zeitpunkt können Sie den Konstruktor gegen einen neuen austauschen.

Beispiel:

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

Das Ersetzen des Konstruktors ist vielleicht etwas dramatisch, aber die Sprache bietet Unterstützung für diese Art der tiefen Selbstbeobachtung und dynamischen Modifikation.

Jarret Hardie
quelle
Danke, das suche ich. Eine Klasse, die eine beliebige Anzahl anderer Klassen so ändern kann, dass sie alle ein bestimmtes Mitglied haben. Meine Gründe dafür, dass die Klassen nicht von einer gemeinsamen ID-Klasse geerbt werden, sind, dass ich sowohl Nicht-ID-Versionen der Klassen als auch ID-Versionen haben möchte.
Robert Gowland
Früher waren Metaklassen die erste Möglichkeit, solche Dinge in Python2.5 oder älter zu tun, aber heutzutage können Sie sehr oft Klassendekoratoren verwenden (siehe Stevens Antwort), die viel einfacher sind.
Jonathan Hartley
202

Abgesehen von der Frage, ob Klassendekorateure die richtige Lösung für Ihr Problem sind:

In Python 2.6 und höher gibt es Klassendekoratoren mit der @ -syntax, sodass Sie schreiben können:

@addID
class Foo:
    pass

In älteren Versionen können Sie dies auf andere Weise tun:

class Foo:
    pass

Foo = addID(Foo)

Beachten Sie jedoch, dass dies genauso funktioniert wie für Funktionsdekorateure und dass der Dekorateur die neue (oder geänderte Original-) Klasse zurückgeben sollte, was im Beispiel nicht der Fall ist. Der addID-Dekorator würde folgendermaßen aussehen:

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

Sie können dann die entsprechende Syntax für Ihre Python-Version wie oben beschrieben verwenden.

Aber ich stimme anderen zu, dass Vererbung besser geeignet ist, wenn Sie überschreiben möchten __init__.

Steven
quelle
2
... obwohl die Frage speziell Python 2.5 erwähnt :-)
Jarret Hardie
5
Entschuldigen Sie das Linesep-Durcheinander, aber Codebeispiele sind in Kommentaren nicht unbedingt großartig ...: 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 hat recht, wenn Sie Ihre Antwort aktualisieren könnten, wäre es viel einfacher, seinen Code zu lesen;)
Tag
3
@ Day: Danke für die Erinnerung, ich hatte die Kommentare vorher nicht bemerkt. Allerdings: Ich bekomme die maximale Rekursionstiefe auch mit Geralds Code überschritten. Ich werde versuchen, eine funktionierende Version zu machen ;-)
Steven
1
@Day und @Gerald: Entschuldigung, das Problem war nicht mit Geralds Code, ich habe es wegen des verstümmelten Codes im Kommentar
Steven
19

Niemand hat erklärt, dass Sie Klassen dynamisch definieren können. Sie können also einen Dekorateur haben, der eine Unterklasse definiert (und zurückgibt):

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

Was in Python 2 verwendet werden kann (der Kommentar von Blckknght, der erklärt, warum Sie dies in 2.6+ fortsetzen sollten) wie folgt:

class Foo:
    pass

FooId = addId(Foo)

Und in Python 3 so (aber seien Sie vorsichtig, wenn Sie es super()in Ihren Klassen verwenden):

@addId
class Foo:
    pass

So können Sie Ihren Kuchen haben und ihn essen - Erbe und Dekorateure!

Andrew Cooke
quelle
4
Unterklassen in einem Dekorator sind in Python 2.6+ gefährlich, da dadurch superAufrufe in der ursprünglichen Klasse unterbrochen werden . Wenn Fooeine Methode mit dem Namen fooaufgerufen worden super(Foo, self).foo()wäre, würde sie sich unendlich wiederholen, da der Name Fooan die vom Dekorateur zurückgegebene Unterklasse gebunden ist, nicht an die ursprüngliche Klasse (auf die unter keinem Namen zugegriffen werden kann). Die Argumentation von Python 3 super()vermeidet dieses Problem (ich gehe davon aus, dass es über dieselbe Compiler-Magie funktioniert, mit der es überhaupt funktioniert). Sie können das Problem auch umgehen, indem Sie die Klasse manuell unter einem anderen Namen dekorieren (wie im Python 2.5-Beispiel).
Blckknght
1
huh. danke, ich hatte keine ahnung (ich benutze python 3). wird einen Kommentar hinzufügen.
Andrew Cooke
13

Das ist keine gute Praxis und deshalb gibt es keinen Mechanismus, um dies zu tun. Der richtige Weg, um das zu erreichen, was Sie wollen, ist die Vererbung.

Schauen Sie sich die Klassendokumentation an .

Ein kleines Beispiel:

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)

Auf diese Weise hat Boss alles Employee, auch seine eigene __init__Methode und seine eigenen Mitglieder.

mpeterson
quelle
3
Ich denke, was ich wollte, war, Boss-Agnostiker der Klasse zu haben, die es enthält. Das heißt, es gibt möglicherweise Dutzende verschiedener Klassen, auf die ich Boss-Funktionen anwenden möchte. Muss ich diese Dutzend Klassen von Boss erben lassen?
Robert Gowland
5
@ Robert Gowland: Deshalb hat Python mehrere Vererbungen. Ja, Sie sollten verschiedene Aspekte von verschiedenen übergeordneten Klassen erben.
S.Lott
7
@ S.Lott: Im Allgemeinen ist Mehrfachvererbung eine schlechte Idee, auch zu viele Vererbungsstufen sind schlecht. Ich werde Ihnen empfehlen, sich von Mehrfachvererbungen fernzuhalten.
Mpeterson
5
mpeterson: Ist die Mehrfachvererbung in Python schlechter als dieser Ansatz? Was ist falsch an der Mehrfachvererbung von Python?
Arafangion
2
@Arafangion: Mehrfachvererbung wird im Allgemeinen als Warnsignal für Probleme angesehen. Dies führt zu komplexen Hierarchien und schwer zu verfolgenden Beziehungen. Wenn sich Ihre Problemdomäne gut für die Mehrfachvererbung eignet (kann sie hierarchisch modelliert werden?), Ist dies für viele eine natürliche Wahl. Dies gilt für alle Sprachen, die Mehrfachvererbung zulassen.
Morten Jensen
6

Ich würde zustimmen, dass Vererbung besser zu dem gestellten Problem passt.

Ich fand diese Frage allerdings sehr praktisch beim Dekorieren von Kursen, danke an alle.

Hier sind einige weitere Beispiele, die auf anderen Antworten basieren, einschließlich der Auswirkungen der Vererbung auf Python 2.7 (und @wraps , das die Dokumentzeichenfolge der ursprünglichen Funktion usw. beibehält):

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

Oft möchten Sie Ihrem Dekorateur Parameter hinzufügen:

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

Ausgänge:

@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

Ich habe einen Funktionsdekorateur verwendet, da ich sie prägnanter finde. Hier ist eine Klasse zum Dekorieren einer Klasse:

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

Eine robustere Version, die nach diesen Klammern sucht und funktioniert, wenn die Methoden für die dekorierte Klasse nicht vorhanden sind:

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

Die assertKontrollen , dass der Dekorateur nicht ohne Klammern verwendet. Wenn dies der Fall ist, wird die zu dekorierende Klasse an den msgParameter des Dekorators übergeben, wodurch ein ausgelöst wird AssertionError.

@decorate_ifgilt nur decoratorwenn das conditionausgewertet wird True.

Die getattr, callabletest und @decorate_ifwerden verwendet, damit der Dekorator nicht kaputt geht, wenn die foo()Methode für die zu dekorierende Klasse nicht vorhanden ist.

Chris
quelle
4

Hier gibt es tatsächlich eine ziemlich gute Implementierung eines Klassendekorateurs:

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

Ich denke tatsächlich, dass dies eine ziemlich interessante Implementierung ist. Da es die Klasse, die es dekoriert, unterordnet, verhält es sich in Dingen wie isinstancePrüfungen genau wie diese Klasse .

__init__Dies hat einen zusätzlichen Vorteil: Es ist nicht ungewöhnlich, dass die Anweisung in einem benutzerdefinierten Django-Formular Änderungen oder Ergänzungen vornimmt, self.fieldssodass Änderungen besser vorgenommen werden können self.fields, nachdem alle __init__für die betreffende Klasse ausgeführt wurden.

Sehr schlau.

In Ihrer Klasse möchten Sie jedoch, dass die Dekoration den Konstruktor ändert, was meiner Meinung nach kein guter Anwendungsfall für einen Klassendekorateur ist.

Jordan Reiter
quelle
0

Hier ist ein Beispiel, das die Frage der Rückgabe der Parameter einer Klasse beantwortet. Darüber hinaus wird die Vererbungskette weiterhin berücksichtigt, dh es werden nur die Parameter der Klasse selbst zurückgegeben. Die Funktion get_paramswird als einfaches Beispiel hinzugefügt, aber dank des Inspect-Moduls können auch andere Funktionen hinzugefügt werden.

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
quelle