Wie man alle Methoden einer Python-Klasse mit einem bestimmten Dekorator erhält

79

Wie bekomme ich alle Methoden einer bestimmten Klasse A, die mit dem @ decorator2 dekoriert sind?

class A():
    def method_a(self):
      pass

    @decorator1
    def method_b(self, b):
      pass

    @decorator2
    def method_c(self, t=5):
      pass
Kraiz
quelle
2
Haben Sie die Kontrolle über den Quellcode von "decorator2"?
Ascobol
8
Sagen wir nein, nur um es interessant zu halten. Aber wenn es die Lösung so viel einfacher macht, interessiert mich auch diese Lösung.
Kraiz
16
+1: "halte es interessant": so mehr lernen
Lauritz V. Thaulow
10
@ S.Lott: Durch Suche weniger lernen , meinst du ? Schauen Sie sich die Top-Antwort unten an. Ist das nicht ein sehr guter Beitrag zu SO, der seinen Wert als Programmiererressource erhöht? Ich behaupte, dass der Hauptgrund, warum diese Antwort so gut ist, darin besteht, dass @kraiz "es interessant halten wollte". Die Antworten auf Ihre verknüpfte Frage enthalten kein Zehntel der in der Antwort unten enthaltenen Informationen, es sei denn, Sie zählen die beiden Links, die hierher zurückführen.
Lauritz V. Thaulow

Antworten:

115

Methode 1: Grundlegende Registrierung Dekorateur

Diese Frage habe ich hier bereits beantwortet: Funktionen über Array-Index in Python aufrufen =)


Methode 2: Analyse des Quellcodes

Wenn Sie keine Kontrolle über das haben Klasse Definition , was eine Interpretation das , was man glauben mag, dann ist dies nicht möglich (ohne Code-Lese-Reflexion), da zum Beispiel könnte der Dekorateur ein No-op Dekorateur sein (wie in meinem verknüpften Beispiel), das lediglich die unveränderte Funktion zurückgibt. (Wenn Sie sich jedoch erlauben, die Dekorateure zu verpacken / neu zu definieren, siehe Methode 3: Konvertieren von Dekorateuren in "selbstbewusst" , finden Sie eine elegante Lösung.)

Es ist ein schrecklich schrecklicher Hack, aber Sie könnten das inspectModul verwenden, um den Quellcode selbst zu lesen und zu analysieren. Dies funktioniert in einem interaktiven Interpreter nicht, da das Inspect-Modul die Angabe des Quellcodes im interaktiven Modus verweigert. Nachfolgend finden Sie jedoch einen Proof of Concept.

#!/usr/bin/python3

import inspect

def deco(func):
    return func

def deco2():
    def wrapper(func):
        pass
    return wrapper

class Test(object):
    @deco
    def method(self):
        pass

    @deco2()
    def method2(self):
        pass

def methodsWithDecorator(cls, decoratorName):
    sourcelines = inspect.getsourcelines(cls)[0]
    for i,line in enumerate(sourcelines):
        line = line.strip()
        if line.split('(')[0].strip() == '@'+decoratorName: # leaving a bit out
            nextLine = sourcelines[i+1]
            name = nextLine.split('def')[1].split('(')[0].strip()
            yield(name)

Es klappt!:

>>> print(list(  methodsWithDecorator(Test, 'deco')  ))
['method']

Beachten Sie, dass man auf das Parsen und die Python-Syntax achten muss, z. B. @decound @deco(...gültige Ergebnisse sind, aber @deco2nicht zurückgegeben werden sollten, wenn wir nur danach fragen 'deco'. Wir stellen fest, dass gemäß der offiziellen Python-Syntax unter http://docs.python.org/reference/compound_stmts.html die Dekoratoren wie folgt lauten:

decorator      ::=  "@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE

Wir atmen erleichtert auf, wenn wir uns nicht mit Fällen wie befassen müssen @(deco). Beachten Sie jedoch, dass dies Ihnen immer noch nicht wirklich hilft, wenn Sie wirklich sehr komplizierte Dekorateure haben, wie @getDecorator(...)z

def getDecorator():
    return deco

Daher kann diese Best-That-You-Do-Strategie zum Parsen von Code solche Fälle nicht erkennen. Wenn Sie diese Methode verwenden, ist das, wonach Sie wirklich suchen, was in der Definition über der Methode steht, in diesem FallgetDecorator .

Gemäß der Spezifikation gilt es auch @foo1.bar2.baz3(...)als Dekorateur zu haben . Sie können diese Methode erweitern, um damit zu arbeiten. Möglicherweise können Sie diese Methode auch erweitern <function object ...>, um mit viel Aufwand einen Namen anstelle des Funktionsnamens zurückzugeben. Diese Methode ist jedoch hackisch und schrecklich.


Methode 3: Konvertierer so umwandeln, dass sie "selbstbewusst" sind

Wenn Sie keine Kontrolle über die haben Dekorateur Definition (die eine andere Interpretation ist , was Sie möchten), dann werden alle diese Probleme gehen weg , weil Sie Kontrolle darüber haben, wie der Dekorateur angewendet wird. So können Sie den Dekorateur von ändern Einwickeln es, Sie zu schaffen eigene Dekorateur, und verwenden Sie, dass Ihre Funktionen zu dekorieren. Lassen Sie mich das noch einmal sagen: Sie können einen Dekorateur erstellen, der den Dekorateur dekoriert, über den Sie keine Kontrolle haben, und ihn "erleuchten", was in unserem Fall dazu führt, dass er das tut, was er zuvor getan hat, aber auch einen.decorator Metadateneigenschaft an den von ihm zurückgegebenen Aufruf anfügt So können Sie verfolgen, ob diese Funktion dekoriert wurde oder nicht. Überprüfen wir function.decorator!Sie können die Methoden der Klasse durchlaufen und überprüfen, ob der Dekorateur über die entsprechende .decoratorEigenschaft verfügt! =) Wie hier gezeigt:

def makeRegisteringDecorator(foreignDecorator):
    """
        Returns a copy of foreignDecorator, which is identical in every
        way(*), except also appends a .decorator property to the callable it
        spits out.
    """
    def newDecorator(func):
        # Call to newDecorator(method)
        # Exactly like old decorator, but output keeps track of what decorated it
        R = foreignDecorator(func) # apply foreignDecorator, like call to foreignDecorator(method) would have done
        R.decorator = newDecorator # keep track of decorator
        #R.original = func         # might as well keep track of everything!
        return R

    newDecorator.__name__ = foreignDecorator.__name__
    newDecorator.__doc__ = foreignDecorator.__doc__
    # (*)We can be somewhat "hygienic", but newDecorator still isn't signature-preserving, i.e. you will not be able to get a runtime list of parameters. For that, you need hackish libraries...but in this case, the only argument is func, so it's not a big issue

    return newDecorator

Demonstration für @decorator:

deco = makeRegisteringDecorator(deco)

class Test2(object):
    @deco
    def method(self):
        pass

    @deco2()
    def method2(self):
        pass

def methodsWithDecorator(cls, decorator):
    """ 
        Returns all methods in CLS with DECORATOR as the
        outermost decorator.

        DECORATOR must be a "registering decorator"; one
        can make any decorator "registering" via the
        makeRegisteringDecorator function.
    """
    for maybeDecorated in cls.__dict__.values():
        if hasattr(maybeDecorated, 'decorator'):
            if maybeDecorated.decorator == decorator:
                print(maybeDecorated)
                yield maybeDecorated

Es klappt!:

>>> print(list(   methodsWithDecorator(Test2, deco)   ))
[<function method at 0x7d62f8>]

Ein "registrierter Dekorateur" muss jedoch der äußerste Dekorateur sein , da sonst die .decoratorAttributanmerkung verloren geht. Zum Beispiel in einem Zug von

@decoOutermost
@deco
@decoInnermost
def func(): ...

Sie können nur Metadaten sehen, die decoOutermostverfügbar gemacht werden, es sei denn, wir behalten Verweise auf "innerere" Wrapper bei.

Nebenbemerkung: Mit der obigen Methode kann auch eine Methode erstellt werden .decorator, die den gesamten Stapel der angewendeten Dekoratoren und Eingabefunktionen sowie die Argumente der Dekorateurfabrik verfolgt . =) Wenn Sie beispielsweise die auskommentierte Zeile berücksichtigen R.original = func, können Sie mit einer solchen Methode alle Wrapper-Ebenen verfolgen. Dies ist persönlich das, was ich tun würde, wenn ich eine Dekorationsbibliothek schreiben würde, da dies eine tiefe Selbstbeobachtung ermöglicht.

Es gibt auch einen Unterschied zwischen @foound @bar(...). Beachten Sie, dass es sich bei beiden um "Dekorator-Expressons" handelt, die in der Spezifikation definiert sind. Beachten Sie jedoch, dass fooes sich um einen Dekorator handelt, während bar(...)ein dynamisch erstellter Dekorator zurückgegeben wird, der dann angewendet wird. Sie benötigen also eine separate Funktion makeRegisteringDecoratorFactory, die etwas ähnlicher ist, makeRegisteringDecoratoraber noch MEHR META:

def makeRegisteringDecoratorFactory(foreignDecoratorFactory):
    def newDecoratorFactory(*args, **kw):
        oldGeneratedDecorator = foreignDecoratorFactory(*args, **kw)
        def newGeneratedDecorator(func):
            modifiedFunc = oldGeneratedDecorator(func)
            modifiedFunc.decorator = newDecoratorFactory # keep track of decorator
            return modifiedFunc
        return newGeneratedDecorator
    newDecoratorFactory.__name__ = foreignDecoratorFactory.__name__
    newDecoratorFactory.__doc__ = foreignDecoratorFactory.__doc__
    return newDecoratorFactory

Demonstration für @decorator(...):

def deco2():
    def simpleDeco(func):
        return func
    return simpleDeco

deco2 = makeRegisteringDecoratorFactory(deco2)

print(deco2.__name__)
# RESULT: 'deco2'

@deco2()
def f():
    pass

Dieser Generator-Factory-Wrapper funktioniert auch:

>>> print(f.decorator)
<function deco2 at 0x6a6408>

Bonus Versuchen wir mit Methode 3 sogar Folgendes:

def getDecorator(): # let's do some dispatching!
    return deco

class Test3(object):
    @getDecorator()
    def method(self):
        pass

    @deco2()
    def method2(self):
        pass

Ergebnis:

>>> print(list(   methodsWithDecorator(Test3, deco)   ))
[<function method at 0x7d62f8>]

Wie Sie sehen, wird @deco im Gegensatz zu Methode2 korrekt erkannt, obwohl es nie explizit in der Klasse geschrieben wurde. Im Gegensatz zu Methode2 funktioniert dies auch, wenn die Methode zur Laufzeit (manuell, über eine Metaklasse usw.) hinzugefügt oder geerbt wird.

Beachten Sie, dass Sie auch eine Klasse dekorieren können. Wenn Sie also einen Dekorator "aufklären", der sowohl zum Dekorieren von Methoden als auch von Klassen verwendet wird, und dann eine Klasse in den Hauptteil der Klasse schreiben, die Sie analysieren möchten , methodsWithDecoratorwerden dekorierte Klassen als zurückgegeben sowie dekorierte Methoden. Man könnte dies als eine Funktion betrachten, aber Sie können leicht Logik schreiben, um diese zu ignorieren, indem Sie das Argument an den Dekorateur untersuchen, dh .originalum die gewünschte Semantik zu erreichen.

Ninjagecko
quelle
1
Dies ist eine so gute Antwort auf ein Problem mit einer nicht offensichtlichen Lösung, dass ich ein Kopfgeld für diese Antwort eröffnet habe. Entschuldigung, ich habe nicht genug Repräsentanten, um dir mehr zu geben!
Niall Douglas
2
@ NiallDouglas: Danke. =) (Ich wusste nicht, wie nach einer kritischen Anzahl von Änderungen eine Antwort automatisch in "Community-Wiki" konvertiert wird, so dass ich für die meisten Upvotes keinen Repräsentanten bekam ... also danke!)
ninjagecko
Hmmm, das scheint nicht zu funktionieren, wenn der ursprüngliche Dekorateur eine Eigenschaft (oder eine modifizierte Form von einer) ist? Irgendwelche Ideen?
Steven Murray
Das ist wirklich eine gute Antwort! Awesome @ninjagecko
Brijesh Lakkad
14

Um die ausgezeichnete Antwort von @ ninjagecko in Methode 2: Quellcode-Analyse zu erweitern, können Sie das astin Python 2.6 eingeführte Modul verwenden, um eine Selbstprüfung durchzuführen, solange das Prüfmodul Zugriff auf den Quellcode hat.

def findDecorators(target):
    import ast, inspect
    res = {}
    def visit_FunctionDef(node):
        res[node.name] = [ast.dump(e) for e in node.decorator_list]

    V = ast.NodeVisitor()
    V.visit_FunctionDef = visit_FunctionDef
    V.visit(compile(inspect.getsource(target), '?', 'exec', ast.PyCF_ONLY_AST))
    return res

Ich habe eine etwas kompliziertere dekorierte Methode hinzugefügt:

@x.y.decorator2
def method_d(self, t=5): pass

Ergebnisse:

> findDecorators(A)
{'method_a': [],
 'method_b': ["Name(id='decorator1', ctx=Load())"],
 'method_c': ["Name(id='decorator2', ctx=Load())"],
 'method_d': ["Attribute(value=Attribute(value=Name(id='x', ctx=Load()), attr='y', ctx=Load()), attr='decorator2', ctx=Load())"]}
Shane Holloway
quelle
1
Nizza, Quellenanalyse richtig gemacht und mit den richtigen Einschränkungen. =) Dies ist vorwärtskompatibel, wenn sie sich jemals dazu entschließen, die Python-Grammatik zu verbessern oder zu korrigieren (z. B. indem die Ausdrucksbeschränkungen für einen Dekorator-Ausdruck aufgehoben werden, was wie ein Versehen erscheint).
Ninjagecko
@ninjagecko Ich bin froh, dass ich nicht die einzige Person bin, die auf die Einschränkung des Dekorationsausdrucks gestoßen ist! Am häufigsten stoße ich darauf, wenn ich einen dekorierten Funktionsverschluss innerhalb einer Methode binde. Verwandelt sich in einen dummen zweistufigen Schritt, um es an eine Variable zu binden ...
Shane Holloway
0

Vielleicht, wenn die Dekorateure nicht zu komplex sind (aber ich weiß nicht, ob es einen weniger hackigen Weg gibt).

def decorator1(f):
    def new_f():
        print "Entering decorator1", f.__name__
        f()
    new_f.__name__ = f.__name__
    return new_f

def decorator2(f):
    def new_f():
        print "Entering decorator2", f.__name__
        f()
    new_f.__name__ = f.__name__
    return new_f


class A():
    def method_a(self):
      pass

    @decorator1
    def method_b(self, b):
      pass

    @decorator2
    def method_c(self, t=5):
      pass

print A.method_a.im_func.func_code.co_firstlineno
print A.method_b.im_func.func_code.co_firstlineno
print A.method_c.im_func.func_code.co_firstlineno

quelle
Leider werden nur die Zeilennummern der folgenden Zeilen zurückgegeben: def new_f():(die erste, Zeile 4), def new_f():(die zweite, Zeile 11) und def method_a(self):. Es wird Ihnen schwer fallen, die gewünschten realen Zeilen zu finden, es sei denn, Sie haben eine Konvention, Ihre Dekorateure immer zu schreiben, indem Sie eine neue Funktion als erste Zeile definieren, und außerdem müssen Sie keine Dokumentzeichenfolgen schreiben ... obwohl Sie dies vermeiden könnten Schreiben Sie Dokumentzeichenfolgen, indem Sie eine Methode verwenden, die die Einrückung überprüft, während sie Zeile für Zeile nach oben verschoben wird, um den Namen des tatsächlichen Dekorateurs zu finden.
Ninjagecko
Selbst bei Änderungen funktioniert dies nicht, wenn sich die definierte Funktion nicht im Dekorator befindet. Es ist auch der Fall, dass ein Dekorateur auch ein aufrufbares Objekt sein kann und diese Methode daher möglicherweise sogar eine Ausnahme auslöst.
Ninjagecko
"... wenn die Dekorateure nicht zu komplex sind ..." - wenn die Zeilennummer für zwei dekorierte Methoden gleich ist, sind sie wahrscheinlich gleich dekoriert. Wahrscheinlich. (Nun, der co_filename sollte auch überprüft werden).
0

Eine einfache Möglichkeit, dieses Problem zu lösen, besteht darin, Code in den Dekorator einzufügen, der jede übergebene Funktion / Methode einem Datensatz (z. B. einer Liste) hinzufügt.

z.B

def deco(foo):
    functions.append(foo)
    return foo

Jetzt wird jede Funktion mit dem Deko- Dekorator zu den Funktionen hinzugefügt .

Thomas King
quelle
0

Ich möchte nicht viel hinzufügen, nur eine einfache Variante von Ninjageckos Methode 2. Sie wirkt Wunder.

Gleicher Code, aber Listenverständnis anstelle eines Generators, was ich brauchte.

def methodsWithDecorator(cls, decoratorName):

    sourcelines = inspect.getsourcelines(cls)[0]
    return [ sourcelines[i+1].split('def')[1].split('(')[0].strip()
                    for i, line in enumerate(sourcelines)
                    if line.split('(')[0].strip() == '@'+decoratorName]
Skovborg Jensen
quelle
0

Wenn Sie die Kontrolle über die Dekoratoren haben, können Sie Dekoratorklassen anstelle von Funktionen verwenden:

class awesome(object):
    def __init__(self, method):
        self._method = method
    def __call__(self, obj, *args, **kwargs):
        return self._method(obj, *args, **kwargs)
    @classmethod
    def methods(cls, subject):
        def g():
            for name in dir(subject):
                method = getattr(subject, name)
                if isinstance(method, awesome):
                    yield name, method
        return {name: method for name,method in g()}

class Robot(object):
   @awesome
   def think(self):
      return 0

   @awesome
   def walk(self):
      return 0

   def irritate(self, other):
      return 0

und wenn ich es nenne awesome.methods(Robot), kehrt es zurück

{'think': <mymodule.awesome object at 0x000000000782EAC8>, 'walk': <mymodulel.awesome object at 0x000000000782EB00>}
Jason S.
quelle