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
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ę.
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())"]}
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
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 .
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]
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>}
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