it-swarm.com.de

Der Python-Dekorator lässt die Funktion vergessen, dass sie zu einer Klasse gehört

Ich versuche einen Dekorateur zu schreiben, um das Logging durchzuführen:

def logger(myFunc):
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__)
        return myFunc(*args, **keyargs)

    return new

class C(object):
    @logger
    def f():
        pass

C().f()

Ich möchte folgendes drucken:

Entering C.f

stattdessen bekomme ich diese Fehlermeldung:

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

Vermutlich hat dies etwas mit dem Umfang von 'myFunc' in 'Logger' zu tun, aber ich habe keine Ahnung, was.

56

Claudius Antwort ist korrekt, aber Sie können auch betrügen, indem Sie den Klassennamen vom Argument self entfernen. Dadurch werden in Vererbungsfällen irreführende Protokollanweisungen ausgegeben, aber die Klasse des Objekts, dessen Methode aufgerufen wird, wird angezeigt. Zum Beispiel:

from functools import wraps  # use this to preserve function signatures and docstrings
def logger(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print "Entering %s.%s" % (args[0].__class__.__name__, func.__name__)
        return func(*args, **kwargs)
    return with_logging

class C(object):
    @logger
    def f(self):
        pass

C().f()

Wie gesagt, das funktioniert nicht richtig, wenn Sie eine Funktion von einer übergeordneten Klasse geerbt haben. In diesem Fall könnte man sagen

class B(C):
    pass

b = B()
b.f()

und rufen Sie die Nachricht Entering B.f auf, wo Sie die Nachricht Entering C.f tatsächlich abrufen möchten, da dies die richtige Klasse ist. Andererseits könnte dies akzeptabel sein. In diesem Fall würde ich diesen Ansatz dem Vorschlag von Claudiu empfehlen.

43
Eli Courtwright

Funktionen werden erst zur Laufzeit zur Methode. Das heißt, wenn Sie C.f erhalten, erhalten Sie eine gebundene Funktion (und C.f.im_class is C). Zum Zeitpunkt Ihrer Definition ist Ihre Funktion nur eine reine Funktion, sie ist an keine Klasse gebunden. Diese ungebundene und nicht zugeordnete Funktion zeichnet Logger aus.

self.__class__.__name__ gibt Ihnen den Namen der Klasse, Sie können jedoch auch Deskriptoren verwenden, um dies etwas allgemeiner zu bewerkstelligen. Dieses Muster wird beschrieben in einem Blogbeitrag zu Decorators und Descriptors , und eine Implementierung Ihres Logger-Decorators würde insbesondere so aussehen:

class logger(object):
    def __init__(self, func):
        self.func = func
    def __get__(self, obj, type=None):
        return self.__class__(self.func.__get__(obj, type))
    def __call__(self, *args, **kw):
        print 'Entering %s' % self.func
        return self.func(*args, **kw)

class C(object):
    @logger
    def f(self, x, y):
        return x+y

C().f(1, 2)
# => Entering <bound method C.f of <__main__.C object at 0x...>>

Offensichtlich kann die Ausgabe verbessert werden (beispielsweise durch Verwendung von getattr(self.func, 'im_class', None)), aber dieses allgemeine Muster funktioniert sowohl für Methoden als auch für Funktionen. Es wird jedoch nicht für Klassen im alten Stil funktionieren (aber diese nicht verwenden;)

25
ianb

Die hier vorgeschlagenen Ideen sind ausgezeichnet, haben jedoch einige Nachteile:

  1. inspect.getouterframes und args[0].__class__.__name__ eignen sich nicht für einfache Funktionen und statische Methoden.
  2. __get__ muss in einer Klasse sein, die von @wraps abgelehnt wird.
  3. @wraps selbst sollte Spuren besser verbergen.

Ich habe also einige Ideen dieser Seite, Links, Dokumente und meinen eigenen Kopf kombiniert.
und schließlich eine Lösung gefunden, der alle drei oben genannten Nachteile fehlen.

Als Ergebnis method_decorator:

  • Kennt die Klasse, an die die verzierte Methode gebunden ist.
  • Blendet Dekorator-Spuren aus, indem Systemattribute korrekter beantwortet werden als functools.wraps().
  • Wird mit Unit-Tests für gebundene ungebundene Instanzmethoden, Klassenmethoden, statische Methoden und einfache Funktionen abgedeckt.

Verwendungszweck:

pip install method_decorator
from method_decorator import method_decorator

class my_decorator(method_decorator):
    # ...

Siehe vollständige Komponententests für Details zur Verwendung .

Und hier ist nur der Code der method_decorator-Klasse:

class method_decorator(object):

    def __init__(self, func, obj=None, cls=None, method_type='function'):
        # These defaults are OK for plain functions
        # and will be changed by __get__() for methods once a method is dot-referenced.
        self.func, self.obj, self.cls, self.method_type = func, obj, cls, method_type

    def __get__(self, obj=None, cls=None):
        # It is executed when decorated func is referenced as a method: cls.func or obj.func.

        if self.obj == obj and self.cls == cls:
            return self # Use the same instance that is already processed by previous call to this __get__().

        method_type = (
            'staticmethod' if isinstance(self.func, staticmethod) else
            'classmethod' if isinstance(self.func, classmethod) else
            'instancemethod'
            # No branch for plain function - correct method_type for it is already set in __init__() defaults.
        )

        return object.__getattribute__(self, '__class__')( # Use specialized method_decorator (or descendant) instance, don't change current instance attributes - it leads to conflicts.
            self.func.__get__(obj, cls), obj, cls, method_type) # Use bound or unbound method with this underlying func.

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def __getattribute__(self, attr_name): # Hiding traces of decoration.
        if attr_name in ('__init__', '__get__', '__call__', '__getattribute__', 'func', 'obj', 'cls', 'method_type'): # Our known names. '__class__' is not included because is used only with explicit object.__getattribute__().
            return object.__getattribute__(self, attr_name) # Stopping recursion.
        # All other attr_names, including auto-defined by system in self, are searched in decorated self.func, e.g.: __module__, __class__, __name__, __doc__, im_*, func_*, etc.
        return getattr(self.func, attr_name) # Raises correct AttributeError if name is not found in decorated self.func.

    def __repr__(self): # Special case: __repr__ ignores __getattribute__.
        return self.func.__repr__()
16
Denis Ryzhkov

Es scheint, dass Python während der Erstellung der Klasse reguläre Funktionsobjekte erstellt. Sie werden erst danach in ungebundene Methodenobjekte umgewandelt. Wenn ich das weiß, ist dies die einzige Möglichkeit, das zu tun, was Sie wollen:

def logger(myFunc):
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (myFunc.im_class.__name__, myFunc.__name__)
        return myFunc(*args, **keyargs)

    return new

class C(object):
    def f(self):
        pass
C.f = logger(C.f)
C().f()

Dies gibt das gewünschte Ergebnis aus.

Wenn Sie alle Methoden in eine Klasse einschließen möchten, möchten Sie wahrscheinlich eine wrapClass-Funktion erstellen, die Sie wie folgt verwenden könnten:

C = wrapClass(C)
7
Claudiu

Ich fand eine andere Lösung für ein sehr ähnliches Problem mit der inspect-Bibliothek. Wenn der Dekorator aufgerufen wird, obwohl die Funktion noch nicht an die Klasse gebunden ist, können Sie den Stapel untersuchen und herausfinden, welche Klasse den Dekorator aufruft. Sie können zumindest den String-Namen der Klasse abrufen, wenn dies alles ist, was Sie benötigen (wahrscheinlich können sie noch nicht referenziert werden, da sie erstellt wird). Dann müssen Sie nach dem Erstellen der Klasse nichts aufrufen.

import inspect

def logger(myFunc):
    classname = inspect.getouterframes(inspect.currentframe())[1][3]
    def new(*args, **keyargs):
        print 'Entering %s.%s' % (classname, myFunc.__name__)
        return myFunc(*args, **keyargs)
    return new

class C(object):
    @logger
    def f(self):
        pass

C().f()

Dies ist zwar nicht unbedingt besser als bei den anderen, aber es ist die only - Weise, auf die ich während des Aufrufs an den Dekorateur den Klassennamen der zukünftigen Methode ermitteln kann. Beachten Sie, dass in der inspect-Bibliotheksdokumentation keine Verweise auf Frames beibehalten werden.

6
user398139

Klassenfunktionen sollten immer selbst als erstes Argument verwendet werden. Sie können das also anstelle von im_class verwenden.

def logger(myFunc):
    def new(self, *args, **keyargs):
        print 'Entering %s.%s' % (self.__class__.__name__, myFunc.__name__)
        return myFunc(self, *args, **keyargs)

    return new 

class C(object):
    @logger
    def f(self):
        pass
C().f()

zuerst wollte ich self.__name__ verwenden, aber das funktioniert nicht, weil die Instanz keinen Namen hat. Sie müssen self.__class__.__name__ verwenden, um den Namen der Klasse zu erhalten.

6
Asa Ayers

Sie können auch new.instancemethod() verwenden, um eine Instanzmethode (entweder gebunden oder ungebunden) aus einer Funktion zu erstellen.

0
Andrew Beyer

Anstatt Dekorationscode zur Definitionszeit einzufügen, verzögern Sie die Ausführung dieses Codes, bis Funktion aufgerufen wird. Das Deskriptorobjekt erleichtert das spätere Einfügen des eigenen Codes zum Zeitpunkt des Zugriffs/Aufrufs:

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

    def __get__(self, obj, type_=None):
        return self.__class__(self.func.__get__(obj, type_), 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)

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

Nun können wir die Klasse sowohl zur Zugriffszeit (__get__) als auch zur Aufrufzeit (__call__) untersuchen. Dieser Mechanismus funktioniert sowohl für einfache Methoden als auch für statische Klassenmethoden:

>>> Foo().foo(1, b=2)
called Foo.foo with args=(1,) kwargs={'b': 2}

Vollständiges Beispiel unter: https://github.com/aurzenligl/study/blob/master/python-robotwrap/Example4.py

0
aurzenligl

Wie in Asa Ayers 'Antwort gezeigt, müssen Sie nicht auf das Klassenobjekt zugreifen. Es kann sich lohnen zu wissen, dass Sie seit Python 3.3 auch __qualname__ verwenden können, wodurch Sie den vollständig qualifizierten Namen erhalten:

>>> def logger(myFunc):
...     def new(*args, **keyargs):
...         print('Entering %s' % myFunc.__qualname__)
...         return myFunc(*args, **keyargs)
... 
...     return new
... 
>>> class C(object):
...     @logger
...     def f(self):
...         pass
... 
>>> C().f()
Entering C.f

Dies hat den zusätzlichen Vorteil, dass es auch bei verschachtelten Klassen funktioniert, wie in diesem Beispiel aus PEP 3155 gezeigt:

>>> class C:
...   def f(): pass
...   class D:
...     def g(): pass
...
>>> C.__qualname__
'C'
>>> C.f.__qualname__
'C.f'
>>> C.D.__qualname__
'C.D'
>>> C.D.g.__qualname__
'C.D.g'

Beachten Sie auch, dass in Python 3 das im_class-Attribut nicht mehr vorhanden ist. Wenn Sie also wirklich auf die Klasse in einem Dekorateur zugreifen möchten, benötigen Sie eine andere Methode. Die Methode, die ich derzeit verwende, beinhaltet object.__set_name__ und ist ausführlich in meiner Antwort auf "Kann ein Python-Dekorator einer Instanzmethode auf die Klasse zugreifen?"

0
tyrion