Jak uzyskać wszystkie metody klasy Pythona z podanym dekoratorem

Jak uzyskać wszystkie metody danej klasy A, które są ozdobione @decorator2?

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

    @decorator1
    def method_b(self, b):
      pass

    @decorator2
    def method_c(self, t=5):
      pass
Author: winhowes, 2011-05-06

6 answers

Metoda 1: podstawowy dekorator rejestracji

Odpowiedziałem już tutaj na to pytanie: wywołanie funkcji przez indeks tablicy w Pythonie =)


Metoda 2: parsowanie kodu źródłowego

jeśli nie masz kontroli nad definicją klasy , która jest jedną z interpretacji tego, co chciałbyś przypuszczać, jest to niemożliwe (bez odczytu kodu), ponieważ na przykład dekorator może być dekoratorem no-op (jak w moim linked przykład), który zwraca jedynie niezmodyfikowaną funkcję. Jeśli jednak pozwolisz sobie zawinąć/przedefiniować dekoratorów, zobacz Metoda 3: przekształcenie dekoratorów w"samoświadomych" , znajdziesz eleganckie rozwiązanie.]}

Jest to straszny hack, ale możesz użyć modułu inspect, aby odczytać sam kod źródłowy i przeanalizować go. Nie będzie to działać w interaktywnym interpreterze, ponieważ moduł inspect odmówi podania kodu źródłowego w trybie interaktywnym. Jednakże, poniżej znajduje się dowód koncepcji.

#!/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)
To działa!:
>>> print(list(  methodsWithDecorator(Test, 'deco')  ))
['method']

Należy zwrócić uwagę na parsowanie i składnię Pythona, np. @deco i @deco(... są poprawnymi wynikami, ale @deco2 nie powinny być zwracane, jeśli tylko poprosimy o 'deco'. Zauważamy, że zgodnie z oficjalną składnią Pythona w http://docs.python.org/reference/compound_stmts.html dekoracje są następujące:

decorator      ::=  "@" dotted_name ["(" [argument_list [","]] ")"] NEWLINE
Odetchnęliśmy z ulgą, nie mając do czynienia z takimi przypadkami jak [18]}. Należy jednak pamiętać, że to nadal nie pomaga, jeśli masz naprawdę bardzo skomplikowane dekoratory, takie jak @getDecorator(...), np.]}
def getDecorator():
    return deco

Tak więc, ta najlepsza strategia parsowania kodu nie może wykryć takich przypadków. Chociaż jeśli używasz tej metody, to tak naprawdę szukasz tego, co jest napisane na górze metody w definicji, która w tym przypadku jest getDecorator.

Zgodnie ze specyfikacją, ważne jest również posiadanie @foo1.bar2.baz3(...) jako dekoratora. Możesz rozszerzyć tę metodę do pracy z tym. Możesz również z dużym wysiłkiem rozszerzyć tę metodę, aby zwrócić <function object ...> zamiast nazwy funkcji. Ta metoda jest jednak hakerska i straszna.


Metoda 3: przekształcanie dekoratorów w "samoświadomych"]}

jeśli nie masz kontroli nad definicją dekoratora (która jest inną interpretacją tego, co chcesz), wszystkie te problemy znikną, ponieważ masz kontrolę nad tym, jak dekorator jest stosowany. W ten sposób można Modyfikuj dekorator przez owijając go , aby stworzyć swójwłasny dekorator i użyj tego do dekoracji swoich funkcji. Pozwól, że powiem jeszcze raz: możesz zrobić dekorator, który dekoruje dekoratora, nad którym nie masz kontroli, "oświecając" go, co w naszym przypadku sprawia, że robi to, co robił wcześniej, ale również dołącza właściwość .decorator metadanych do zwracanego wywołania, pozwalając na śledzenie "czy ta funkcja była dekorowana, czy nie? sprawdźmy. funkcja.dekorator!". I następnie możesz iterować nad metodami klasy i po prostu sprawdzić, czy dekorator ma odpowiednią właściwość .decorator! = ) Jak pokazano tutaj:

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

Demonstracja dla @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
To działa!:
>>> print(list(   methodsWithDecorator(Test2, deco)   ))
[<function method at 0x7d62f8>]

Jednakże, "zarejestrowany dekorator" musi być najbardziej oddalonym dekoratorem , w przeciwnym razie adnotacja atrybutu .decorator zostanie utracona. Na przykład w pociągu

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

Możesz zobaczyć tylko metadane to decoOutermost ujawnia, chyba że zachowamy odniesienia do" bardziej wewnętrznych " opakowań.

Uwaga boczna: powyższa metoda może również zbudować .decorator, który śledzi cały stos zastosowanych dekoratorów i funkcji wejściowych oraz argumentów decorator-factory . = ) Na przykład, jeśli weźmiemy pod uwagę skomentowaną linię R.original = func, możliwe jest użycie metody takiej jak ta do śledzenia wszystkich warstw wrappera. Osobiście tak bym zrobił, gdybym napisał bibliotekę dekoratorów, ponieważ pozwala ona na głęboka introspekcja.

Istnieje również różnica między @foo i @bar(...). Chociaż oba są "dekoratorami expressonów" zdefiniowanymi w specyfikacji, zauważ, że foo jest dekoratorem, podczas gdy bar(...) zwraca dynamicznie tworzony dekorator, który jest następnie stosowany. Dlatego potrzebujesz osobnej funkcji makeRegisteringDecoratorFactory, która jest nieco podobna do makeRegisteringDecorator, ale jeszcze bardziej 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

Demonstracja dla @decorator(...):

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

deco2 = makeRegisteringDecoratorFactory(deco2)

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

@deco2()
def f():
    pass

Ten generator-fabryczna owijarka również działa:

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

Bonus spróbujmy nawet z metodą #3:

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

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

    @deco2()
    def method2(self):
        pass

Wynik:

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

Jak widzisz, w przeciwieństwie do method2, @deco jest poprawnie rozpoznawany, mimo że nigdy nie został wyraźnie napisany w klasie. W przeciwieństwie do method2, będzie to również działać, jeśli metoda zostanie dodana w czasie wykonywania (ręcznie, przez metaklasę, itd.) lub dziedziczne.

Pamiętaj, że możesz również ozdobić klasę, więc jeśli "oświecisz" dekoratora, który jest przyzwyczajony do obu dekoruj metody i klasy, a następnie napisz klasę w ciele klasy, którą chcesz przeanalizować , a następnie methodsWithDecorator zwróci zarówno dekorowane klasy, jak i dekorowane metody. Można to uznać za cechę, ale można łatwo napisać logikę, aby je zignorować, badając argument do dekoratora, tj. {38]}, aby osiągnąć pożądaną semantykę.

 117
Author: ninjagecko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:33

Aby rozwinąć doskonałą odpowiedź @ ninjagecko w Method 2: Source code parsing, możesz użyć modułu ast wprowadzonego w Pythonie 2.6, aby wykonać samokontrolę tak długo, jak moduł inspect ma dostęp do kodu źródłowego.

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

Dodałem nieco bardziej skomplikowaną metodę dekorowania:

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

Wyniki:

> 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())"]}
 15
Author: Shane Holloway,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-03-06 08:11:45

Może, jeśli dekoratorzy nie są zbyt skomplikowani (ale nie wiem, czy jest mniej hakerski sposób).

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
 0
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-06 12:42:56

Prostym sposobem rozwiązania tego problemu jest umieszczenie kodu w dekoratorze, który dodaje każdą przekazaną funkcję/metodę do zbioru danych(na przykład listy).

Np.

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

Teraz każda funkcja z dekoratorem deco zostanie dodana do funkcji .

 0
Author: Thomas King,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-01-25 21:17:58

Nie chcę wiele dodawać, tylko prosta odmiana metody ninjagecko 2. Działa cuda.

Ten sam kod, ale używając rozumienia listy zamiast generatora, czego potrzebowałem.

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]
 0
Author: Skovborg Jensen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-07-15 21:01:27

Jeśli masz kontrolę nad dekoratorami, możesz używać klas dekoratorów zamiast funkcji:

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

And if I call awesome.methods(Robot) it returns

{'think': <mymodule.awesome object at 0x000000000782EAC8>, 'walk': <mymodulel.awesome object at 0x000000000782EB00>}
 0
Author: Jason S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-10-09 20:33:23