Kann ein Dekorateur einer Instanzmethode auf die Klasse zugreifen?


108

Hallo, ich habe ungefähr so ​​etwas wie das Folgende. Grundsätzlich muss ich von einem Dekorator, der für die Instanzmethode in ihrer Definition verwendet wird, auf die Klasse einer Instanzmethode zugreifen.

def decorator(view):
    # do something that requires view's class
    print view.im_class
    return view

class ModelA(object):
    @decorator
    def a_method(self):
        # do some stuff
        pass

Der Code wie er ist gibt:

AttributeError: 'function' object has no attribute 'im_class'

Ich habe ähnliche Fragen / Antworten gefunden - Python Decorator lässt die Funktion vergessen, dass sie zu einer Klasse gehört, und Get Class in Python Decorator - aber diese basieren auf einer Problemumgehung, die die Instanz zur Laufzeit erfasst, indem der erste Parameter abgerufen wird. In meinem Fall rufe ich die Methode basierend auf den Informationen auf, die aus ihrer Klasse stammen, sodass ich nicht warten kann, bis ein Aufruf eingeht.

Antworten:


68

Wenn Sie Python 2.6 oder höher verwenden, können Sie einen Klassendekorator verwenden, vielleicht so etwas (Warnung: ungetesteter Code).

def class_decorator(cls):
   for name, method in cls.__dict__.iteritems():
        if hasattr(method, "use_class"):
            # do something with the method and class
            print name, cls
   return cls

def method_decorator(view):
    # mark the method as something that requires view's class
    view.use_class = True
    return view

@class_decorator
class ModelA(object):
    @method_decorator
    def a_method(self):
        # do some stuff
        pass

Der Methodendekorateur markiert die Methode als eine Methode, die von Interesse ist, indem er ein Attribut "use_class" hinzufügt. Funktionen und Methoden sind ebenfalls Objekte, sodass Sie ihnen zusätzliche Metadaten hinzufügen können.

Nachdem die Klasse erstellt wurde, durchläuft der Klassendekorateur alle Methoden und führt alles aus, was für die markierten Methoden erforderlich ist.

Wenn Sie möchten, dass alle Methoden betroffen sind, können Sie den Methodendekorator weglassen und einfach den Klassendekorator verwenden.


2
Danke, ich denke, das ist der Weg, den man gehen muss. Nur eine zusätzliche Codezeile für jede Klasse, die ich mit diesem Dekorateur verwenden möchte. Vielleicht könnte ich eine benutzerdefinierte Metaklasse verwenden und diese Überprüfung während eines neuen ... durchführen?
Carl G

3
Jeder, der versucht, dies mit staticmethod oder classmethod zu verwenden, sollte diesen PEP lesen: python.org/dev/peps/pep-0232 Nicht sicher, ob dies möglich ist, da Sie kein Attribut für eine class / static-Methode festlegen können und ich denke, dass sie verschlingen Erhöhen Sie alle benutzerdefinierten Funktionsattribute, wenn sie auf eine Funktion angewendet werden.
Carl G

Genau das, wonach ich gesucht habe, für mein DBM-basiertes ORM ... Danke, Alter.
Coyote21

Sie sollten verwenden inspect.getmro(cls), um alle Basisklassen im Klassendekorator zu verarbeiten, um die Vererbung zu unterstützen.
Schlamar

1
Oh, eigentlich sieht es inspectnach dem Rescue Stackoverflow.com/a/1911287/202168 aus
Anentropic

16

Seit Python 3.6 können Sie object.__set_name__dies auf sehr einfache Weise erreichen. Das Dokument gibt an, dass __set_name__"zum Zeitpunkt der Erstellung des Besitzers der besitzenden Klasse aufgerufen wird". Hier ist ein Beispiel:

class class_decorator:
    def __init__(self, fn):
        self.fn = fn

    def __set_name__(self, owner, name):
        # do something with owner, i.e.
        print(f"decorating {self.fn} and using {owner}")
        self.fn.class_name = owner.__name__

        # then replace ourself with the original method
        setattr(owner, name, self.fn)

Beachten Sie, dass es zur Zeit der Klassenerstellung aufgerufen wird:

>>> class A:
...     @class_decorator
...     def hello(self, x=42):
...         return x
...
decorating <function A.hello at 0x7f9bedf66bf8> and using <class '__main__.A'>
>>> A.hello
<function __main__.A.hello(self, x=42)>
>>> A.hello.class_name
'A'
>>> a = A()
>>> a.hello()
42

Wenn Sie mehr darüber erfahren möchten, wie Klassen erstellt werden und insbesondere wann genau __set_name__aufgerufen wird, lesen Sie die Dokumentation zum Thema "Erstellen des Klassenobjekts" .


Wie würde das aussehen, wenn der Dekorator mit Parametern verwendet würde? ZB@class_decorator('test', foo='bar')
Luckydonald

2
@luckydonald Sie können es ähnlich wie normale Dekorateure angehen , die Argumente annehmen . Nur habendef decorator(*args, **kwds): class Descriptor: ...; return Descriptor
Matt Eding

Wow, vielen Dank. __set_name__Ich wusste es nicht, obwohl ich Python 3.6+ schon lange benutze.
Kawing-Chiu

Es gibt einen Nachteil dieser Methode: Der statische Prüfer versteht dies überhaupt nicht. Mypy wird denken, dass dies hellokeine Methode ist, sondern ein Objekt vom Typ class_decorator.
Kawing-Chiu

@ kawing-chiu Wenn nichts anderes funktioniert, können Sie a verwenden if TYPE_CHECKING, um class_decoratorals normalen Dekorateur den richtigen Typ zurückzugeben.
Tyrion

15

Wie andere bereits betont haben, wurde die Klasse zum Zeitpunkt des Aufrufs des Dekorateurs noch nicht erstellt. Jedoch ist es möglich , die Funktion Objekt mit den decorator Parametern mit Anmerkungen zu versehen, dann die Funktion in der der Metaklasse Wieder dekoriert __new__Methode. Sie müssen __dict__direkt auf das Attribut der Funktion zugreifen , was zumindest für mich func.foo = 1zu einem AttributeError geführt hat.


6
setattrsollte verwendet werden, anstatt zuzugreifen__dict__
schlamar

7

Wie Mark vorschlägt:

  1. Jeder Dekorateur heißt BEVOR die Klasse gebaut wird, ist dem Dekorateur also unbekannt.
  2. Wir können diese Methoden markieren und später alle erforderlichen Nachbearbeitungen vornehmen.
  3. Wir haben zwei Möglichkeiten für die Nachbearbeitung: automatisch am Ende der Klassendefinition oder irgendwo bevor die Anwendung ausgeführt wird. Ich bevorzuge die 1. Option mit einer Basisklasse, aber Sie können auch dem 2. Ansatz folgen.

Dieser Code zeigt, wie dies mit der automatischen Nachbearbeitung funktionieren kann:

def expose(**kw):
    "Note that using **kw you can tag the function with any parameters"
    def wrap(func):
        name = func.func_name
        assert not name.startswith('_'), "Only public methods can be exposed"

        meta = func.__meta__ = kw
        meta['exposed'] = True
        return func

    return wrap

class Exposable(object):
    "Base class to expose instance methods"
    _exposable_ = None  # Not necessary, just for pylint

    class __metaclass__(type):
        def __new__(cls, name, bases, state):
            methods = state['_exposed_'] = dict()

            # inherit bases exposed methods
            for base in bases:
                methods.update(getattr(base, '_exposed_', {}))

            for name, member in state.items():
                meta = getattr(member, '__meta__', None)
                if meta is not None:
                    print "Found", name, meta
                    methods[name] = member
            return type.__new__(cls, name, bases, state)

class Foo(Exposable):
    @expose(any='parameter will go', inside='__meta__ func attribute')
    def foo(self):
        pass

class Bar(Exposable):
    @expose(hide=True, help='the great bar function')
    def bar(self):
        pass

class Buzz(Bar):
    @expose(hello=False, msg='overriding bar function')
    def bar(self):
        pass

class Fizz(Foo):
    @expose(msg='adding a bar function')
    def bar(self):
        pass

print('-' * 20)
print("showing exposed methods")
print("Foo: %s" % Foo._exposed_)
print("Bar: %s" % Bar._exposed_)
print("Buzz: %s" % Buzz._exposed_)
print("Fizz: %s" % Fizz._exposed_)

print('-' * 20)
print('examine bar functions')
print("Bar.bar: %s" % Bar.bar.__meta__)
print("Buzz.bar: %s" % Buzz.bar.__meta__)
print("Fizz.bar: %s" % Fizz.bar.__meta__)

Die Ausgabe ergibt:

Found foo {'inside': '__meta__ func attribute', 'any': 'parameter will go', 'exposed': True}
Found bar {'hide': True, 'help': 'the great bar function', 'exposed': True}
Found bar {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Found bar {'msg': 'adding a bar function', 'exposed': True}
--------------------
showing exposed methods
Foo: {'foo': <function foo at 0x7f7da3abb398>}
Bar: {'bar': <function bar at 0x7f7da3abb140>}
Buzz: {'bar': <function bar at 0x7f7da3abb0c8>}
Fizz: {'foo': <function foo at 0x7f7da3abb398>, 'bar': <function bar at 0x7f7da3abb488>}
--------------------
examine bar functions
Bar.bar: {'hide': True, 'help': 'the great bar function', 'exposed': True}
Buzz.bar: {'msg': 'overriding bar function', 'hello': False, 'exposed': True}
Fizz.bar: {'msg': 'adding a bar function', 'exposed': True}

Beachten Sie, dass in diesem Beispiel:

  1. Wir können jede Funktion mit beliebigen Parametern versehen.
  2. Jede Klasse hat ihre eigenen Methoden.
  3. Wir können auch exponierte Methoden erben.
  4. Methoden können überschrieben werden, wenn die Offenlegungsfunktion aktualisiert wird.

Hoffe das hilft


4

Wie Ants angegeben hat, können Sie innerhalb der Klasse keinen Verweis auf die Klasse erhalten. Wenn Sie jedoch zwischen verschiedenen Klassen unterscheiden möchten (ohne das eigentliche Klassentypobjekt zu manipulieren), können Sie für jede Klasse eine Zeichenfolge übergeben. Sie können dem Dekorateur auch beliebige andere Parameter mit Dekoratoren im Klassenstil übergeben.

class Decorator(object):
    def __init__(self,decoratee_enclosing_class):
        self.decoratee_enclosing_class = decoratee_enclosing_class
    def __call__(self,original_func):
        def new_function(*args,**kwargs):
            print 'decorating function in ',self.decoratee_enclosing_class
            original_func(*args,**kwargs)
        return new_function


class Bar(object):
    @Decorator('Bar')
    def foo(self):
        print 'in foo'

class Baz(object):
    @Decorator('Baz')
    def foo(self):
        print 'in foo'

print 'before instantiating Bar()'
b = Bar()
print 'calling b.foo()'
b.foo()

Drucke:

before instantiating Bar()
calling b.foo()
decorating function in  Bar
in foo

Außerdem sehen Bruce Eckel Seite auf Dekorateure.


Vielen Dank für die Bestätigung meiner deprimierenden Schlussfolgerung, dass dies nicht möglich ist. Ich könnte auch eine Zeichenfolge verwenden, die das Modul / die Klasse vollständig qualifiziert ('module.Class'), die Zeichenfolge (n) speichern, bis alle Klassen vollständig geladen sind, und dann die Klassen selbst mit Import abrufen. Das scheint eine kläglich ungetrocknete Art zu sein, meine Aufgabe zu erfüllen.
Carl G.

Sie müssen für diese Art von Dekorator keine Klasse verwenden: Der idiomatische Ansatz besteht darin, eine zusätzliche Ebene verschachtelter Funktionen innerhalb der Dekoratorfunktion zu verwenden. Wenn Sie sich jedoch für Klassen entscheiden, ist es möglicherweise besser, die Groß- und Kleinschreibung im Klassennamen nicht zu verwenden, damit die Dekoration selbst "Standard" aussieht, dh @decorator('Bar')im Gegensatz zu @Decorator('Bar').
Erik Kaplun

4

Was flaschen nobel tut , ist eine temporäre Cache erstellen , dass es auf der Methode speichert, verwendet es etwas anderes (die Tatsache , dass Flask werden die Klassen mit einem Register registerKlassenmethode) , um tatsächlich die Methode wickelt.

Sie können dieses Muster wiederverwenden, diesmal mithilfe einer Metaklasse, damit Sie die Methode beim Import umbrechen können.

def route(rule, **options):
    """A decorator that is used to define custom routes for methods in
    FlaskView subclasses. The format is exactly the same as Flask's
    `@app.route` decorator.
    """

    def decorator(f):
        # Put the rule cache on the method itself instead of globally
        if not hasattr(f, '_rule_cache') or f._rule_cache is None:
            f._rule_cache = {f.__name__: [(rule, options)]}
        elif not f.__name__ in f._rule_cache:
            f._rule_cache[f.__name__] = [(rule, options)]
        else:
            f._rule_cache[f.__name__].append((rule, options))

        return f

    return decorator

In der tatsächlichen Klasse (Sie können dasselbe mit einer Metaklasse tun):

@classmethod
def register(cls, app, route_base=None, subdomain=None, route_prefix=None,
             trailing_slash=None):

    for name, value in members:
        proxy = cls.make_proxy_method(name)
        route_name = cls.build_route_name(name)
        try:
            if hasattr(value, "_rule_cache") and name in value._rule_cache:
                for idx, cached_rule in enumerate(value._rule_cache[name]):
                    # wrap the method here

Quelle: https://github.com/apiguy/flask-classy/blob/master/flask_classy.py


Das ist ein nützliches Muster, aber dies behebt nicht das Problem, dass ein Methodendekorateur auf die übergeordnete Klasse der Methode verweisen kann, auf die er angewendet wird
Anentropic

Ich habe meine Antwort aktualisiert, um genauer zu erläutern, wie dies nützlich sein kann, um beim Import auf die Klasse zuzugreifen (dh mithilfe einer Metaklasse + Zwischenspeichern des Dekorationsparameters für die Methode).
Charlax

3

Das Problem ist, dass die Klasse noch nicht existiert, wenn der Dekorateur aufgerufen wird. Versuche dies:

def loud_decorator(func):
    print("Now decorating %s" % func)
    def decorated(*args, **kwargs):
        print("Now calling %s with %s,%s" % (func, args, kwargs))
        return func(*args, **kwargs)
    return decorated

class Foo(object):
    class __metaclass__(type):
        def __new__(cls, name, bases, dict_):
            print("Creating class %s%s with attributes %s" % (name, bases, dict_))
            return type.__new__(cls, name, bases, dict_)

    @loud_decorator
    def hello(self, msg):
        print("Hello %s" % msg)

Foo().hello()

Dieses Programm gibt Folgendes aus:

Now decorating <function hello at 0xb74d35dc>
Creating class Foo(<type 'object'>,) with attributes {'__module__': '__main__', '__metaclass__': <class '__main__.__metaclass__'>, 'hello': <function decorated at 0xb74d356c>}
Now calling <function hello at 0xb74d35dc> with (<__main__.Foo object at 0xb74ea1ac>, 'World'),{}
Hello World

Wie Sie sehen, müssen Sie einen anderen Weg finden, um das zu tun, was Sie wollen.


Wenn man eine Funktion definiert, existiert die Funktion noch nicht, aber man kann die Funktion rekursiv aus sich heraus aufrufen. Ich denke, dies ist eine funktionsspezifische Sprachfunktion, die Klassen nicht zur Verfügung steht.
Carl G

DGGenuine: Die Funktion wird nur aufgerufen und die Funktion greift somit erst dann auf sich selbst zu, wenn sie vollständig erstellt wurde. In diesem Fall kann die Klasse beim Aufruf des Dekorators nicht vollständig sein, da die Klasse auf das Ergebnis des Dekorators warten muss, das als eines der Attribute der Klasse gespeichert wird.
u0b34a0f6ae

3

Hier ist ein einfaches Beispiel:

def mod_bar(cls):
    # returns modified class

    def decorate(fcn):
        # returns decorated function

        def new_fcn(self):
            print self.start_str
            print fcn(self)
            print self.end_str

        return new_fcn

    cls.bar = decorate(cls.bar)
    return cls

@mod_bar
class Test(object):
    def __init__(self):
        self.start_str = "starting dec"
        self.end_str = "ending dec" 

    def bar(self):
        return "bar"

Die Ausgabe ist:

>>> import Test
>>> a = Test()
>>> a.bar()
starting dec
bar
ending dec

1

Dies ist eine alte Frage, die aber auf Venus gestoßen ist. http://venusian.readthedocs.org/en/latest/

Es scheint die Fähigkeit zu haben, Methoden zu dekorieren und Ihnen dabei Zugriff auf die Klasse und die Methode zu gewähren. Beachten Sie, dass das Aufrufen setattr(ob, wrapped.__name__, decorated)nicht die typische Art der Verwendung von Venusian ist und den Zweck etwas zunichte macht.

So oder so ... das folgende Beispiel ist vollständig und sollte ausgeführt werden.

import sys
from functools import wraps
import venusian

def logged(wrapped):
    def callback(scanner, name, ob):
        @wraps(wrapped)
        def decorated(self, *args, **kwargs):
            print 'you called method', wrapped.__name__, 'on class', ob.__name__
            return wrapped(self, *args, **kwargs)
        print 'decorating', '%s.%s' % (ob.__name__, wrapped.__name__)
        setattr(ob, wrapped.__name__, decorated)
    venusian.attach(wrapped, callback)
    return wrapped

class Foo(object):
    @logged
    def bar(self):
        print 'bar'

scanner = venusian.Scanner()
scanner.scan(sys.modules[__name__])

if __name__ == '__main__':
    t = Foo()
    t.bar()

1

Die Funktion weiß nicht, ob es sich um eine Methode am Definitionspunkt handelt, wenn der Dekorationscode ausgeführt wird. Nur wenn auf ihn über die Klassen- / Instanzkennung zugegriffen wird, kann er seine Klasse / Instanz kennen. Um diese Einschränkung zu überwinden, können Sie nach Deskriptorobjekt dekorieren, um den tatsächlichen Dekorationscode bis zur Zugriffs- / Anrufzeit zu verzögern:

class decorated(object):
    def __init__(self, func, type_=None):
        self.func = func
        self.type = type_

    def __get__(self, obj, type_=None):
        func = self.func.__get__(obj, type_)
        print('accessed %s.%s' % (type_.__name__, func.__name__))
        return self.__class__(func, type_)

    def __call__(self, *args, **kwargs):
        name = '%s.%s' % (self.type.__name__, self.func.__name__)
        print('called %s with args=%s kwargs=%s' % (name, args, kwargs))
        return self.func(*args, **kwargs)

Auf diese Weise können Sie einzelne (statische | Klassen-) Methoden dekorieren:

class Foo(object):
    @decorated
    def foo(self, a, b):
        pass

    @decorated
    @staticmethod
    def bar(a, b):
        pass

    @decorated
    @classmethod
    def baz(cls, a, b):
        pass

class Bar(Foo):
    pass

Jetzt können Sie Dekorationscode für die Selbstbeobachtung verwenden ...

>>> Foo.foo
accessed Foo.foo
>>> Foo.bar
accessed Foo.bar
>>> Foo.baz
accessed Foo.baz
>>> Bar.foo
accessed Bar.foo
>>> Bar.bar
accessed Bar.bar
>>> Bar.baz
accessed Bar.baz

... und zur Änderung des Funktionsverhaltens:

>>> Foo().foo(1, 2)
accessed Foo.foo
called Foo.foo with args=(1, 2) kwargs={}
>>> Foo.bar(1, b='bcd')
accessed Foo.bar
called Foo.bar with args=(1,) kwargs={'b': 'bcd'}
>>> Bar.baz(a='abc', b='bcd')
accessed Bar.baz
called Bar.baz with args=() kwargs={'a': 'abc', 'b': 'bcd'}

Leider ist dieser Ansatz funktional äquivalent zu Will McCutchen ist ebenso nicht anwendbar Antwort . Dies und die Antwort erhält die gewünschte Klasse bei Methode Aufruf Zeit statt Verfahren Dekoration Zeit, wie sie in der ursprünglichen Frage erforderlich. Das einzig vernünftige Mittel, um diese Klasse zu einem ausreichend frühen Zeitpunkt zu erhalten, besteht darin, alle Methoden zum Zeitpunkt der Klassendefinition (z. B. über einen Klassendekorateur oder eine Metaklasse) zu überprüfen. </sigh>
Cecil Curry

1

Wie andere Antworten gezeigt haben, ist Decorator eine funktionale Sache. Sie können nicht auf die Klasse zugreifen, zu der diese Methode gehört, da die Klasse noch nicht erstellt wurde. Es ist jedoch völlig in Ordnung, einen Dekorator zu verwenden, um die Funktion zu "markieren" und dann Metaklassentechniken zu verwenden, um später mit der Methode umzugehen, da __new__die Klasse zu diesem Zeitpunkt von ihrer Metaklasse erstellt wurde.

Hier ist ein einfaches Beispiel:

Wir verwenden @field, um die Methode als spezielles Feld zu markieren und sie in der Metaklasse zu behandeln.

def field(fn):
    """Mark the method as an extra field"""
    fn.is_field = True
    return fn

class MetaEndpoint(type):
    def __new__(cls, name, bases, attrs):
        fields = {}
        for k, v in attrs.items():
            if inspect.isfunction(v) and getattr(k, "is_field", False):
                fields[k] = v
        for base in bases:
            if hasattr(base, "_fields"):
                fields.update(base._fields)
        attrs["_fields"] = fields

        return type.__new__(cls, name, bases, attrs)

class EndPoint(metaclass=MetaEndpoint):
    pass


# Usage

class MyEndPoint(EndPoint):
    @field
    def foo(self):
        return "bar"

e = MyEndPoint()
e._fields  # {"foo": ...}

Sie haben einen Tippfehler in dieser Zeile: if inspect.isfunction(v) and getattr(k, "is_field", False):Es sollte getattr(v, "is_field", False)stattdessen sein.
EvilTosha

0

Sie haben Zugriff auf die Klasse des Objekts, für das die Methode in der dekorierten Methode aufgerufen wird, die Ihr Dekorator zurückgeben soll. Wie so:

def decorator(method):
    # do something that requires view's class
    def decorated(self, *args, **kwargs):
        print 'My class is %s' % self.__class__
        method(self, *args, **kwargs)
    return decorated

Mit Ihrer ModelA-Klasse können Sie Folgendes tun:

>>> obj = ModelA()
>>> obj.a_method()
My class is <class '__main__.ModelA'>

1
Vielen Dank, aber dies ist genau die Lösung, auf die ich in meiner Frage verwiesen habe und die für mich nicht funktioniert. Ich versuche, ein Beobachtermuster mithilfe von Dekoratoren zu implementieren, und ich kann die Methode von meinem Beobachtungs-Dispatcher aus niemals im richtigen Kontext aufrufen, wenn ich die Klasse irgendwann nicht mehr habe, während ich die Methode dem Beobachtungs-Dispatcher hinzufüge. Das Abrufen der Klasse beim Methodenaufruf hilft mir nicht, die Methode überhaupt richtig aufzurufen.
Carl G

Tut mir leid für meine Faulheit, nicht Ihre gesamte Frage zu lesen.
Will McCutchen
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.