How to get all Python class methods with this decorator

How to get all methods of this class A decorated with @ decorator2?

class A(): def method_a(self): pass @decorator1 def method_b(self, b): pass @decorator2 def method_c(self, t=5): pass 
+73
python methods class decorator
May 6 '11 at 11:23
source share
5 answers

Method 1: Basic registration registrar

I already answered this question here: Calling functions by array index in Python =)




Method 2: Parsing Source Code

If you don't have control over the class definition, which is one interpretation of what you would like to suggest, this is impossible (without reading the code, reflection), because, for example, a decorator may be a non-op decorator (as in my related example) which simply returns an unmodified function. (However, if you allow yourself to wrap / redefine decorators, see Method 3: Convert Decorators to "Self-Identity" , then you will find an elegant solution)

This is a terrible terrible hack, but you can use the inspect module to read the source code itself and analyze it. This will not work in the interactive interpreter, because the verification module will refuse to provide the source code interactively. However, below is a 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) 

It works!:

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

Note that you need to pay attention to parsing and python syntax, for example. @deco and @deco(... are valid results, but @deco2 should not be returned if we just request 'deco' . We note that according to the official python syntax at http://docs.python.org/reference/compound_stmts .html decorators look like this:

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

We breathed a sigh of relief, not dealing with cases like @(deco) . But note that this still will not help you if you really have very complex decorators like @getDecorator(...) for example.

 def getDecorator(): return deco 

Therefore, this strategy that is best for you cannot detect such cases. Although, if you use this method, what you really need is what is written on top of the method in the definition, which in this case is getDecorator .

According to the specification, it is also fair to have @foo1.bar2.baz3(...) as a decorator. You can extend this method to work with this. You can also extend this method to return a <function object ...> , rather than a function name, with great difficulty. However, this method is hacky and terrible.




Method 3: Convert Decorators to Self-Identity

If you do not control the definition of decorator (which is a different interpretation of what you would like), then all these problems disappear because you control how the decorator is applied. Thus, you can change the decorator by wrapping it, create your own decorator and use it to decorate your functions. Let me say that again: you can make a decorator that decorates a decorator that you don’t control, enlighten it, which in our case does as it did before, but also adds the .decorator metadata .decorator to the one it .decorator , allowing you to track "was this function decorated or not? let check function.decorator!". And then , you can iterate over class methods and just check if the decorator has the corresponding .decorator property! =) As shown here:

 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, ie 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 not a big issue return newDecorator 

Demo for @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 

It works!:

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

However, the “registered decorator” must be an external designer , otherwise the .decorator annotation will be lost. For example, in a train

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

you can only see the metadata that decoOutermost provides if we do not reference the “more internal” wrappers.

sidenote: the above method can also create a .decorator that tracks the entire stack of applied decorators and input functions and decorator-factory arguments . =) For example, if you are considering a missing line R.original = func , you can use this method to track all layers of the shell. This is personally what I would do if I wrote a decorator library, because it allows a deep introspection.

There is also a difference between @foo and @bar(...) . While they are "decorator idols," as defined in the specification, note that foo is a decorator, and bar(...) returns a dynamically created decorator, which is then applied. Thus, you will need a separate makeRegisteringDecoratorFactory function, which is somewhat similar to makeRegisteringDecorator , but even MORE 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 

Demo for @decorator(...) :

 def deco2(): def simpleDeco(func): return func return simpleDeco deco2 = makeRegisteringDecoratorFactory(deco2) print(deco2.__name__) # RESULT: 'deco2' @deco2() def f(): pass 

This shell generator factory also works:

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

bonus Even try the following with method # 3:

 def getDecorator(): # let do some dispatching! return deco class Test3(object): @getDecorator() def method(self): pass @deco2() def method2(self): pass 

Result:

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

As you can see, unlike method2, @deco is correctly recognized, although it was never explicitly written in the class. Unlike method2, this will also work if the method is added at runtime (manually, through a metaclass, etc.) or inherited.

Remember that you can also decorate a class, so if you enlighten a decorator that is used to decorate methods and classes, then write the class in the body of the class you want to analyze, then methodsWithDecorator will return the decorated classes, and also decorated methods. You can consider this function, but you can easily write logic to ignore it by examining the argument to the decorator, i.e. .original , to achieve the desired semantics.

+107
May 6 '11 at 11:41
source share

To extend @ninjagecko's excellent answer in method 2: Parsing the source code, you can use the ast module introduced in Python 2.6 to perform a self-test if the validator has access to the source code.

 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 

I added a slightly more complicated decorated method:

 @xydecorator2 def method_d(self, t=5): pass 

Results:

 > 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())"]} 
+14
Mar 06 '12 at 8:11
source share

Perhaps if the decorators are not too complicated (but I don't know if there is a less hacky way).

 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
May 6 '11 at 12:42
source share

An easy way to solve this problem is to put the code in a decorator, which adds each function / method passed to the data set (for example, a list).

eg

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

Now each function with a Deco decorator will be added to the functions.

0
Jan 25 '19 at 20:48
source share

I don’t want to add much, just a simple variant of the ninjagecko 2 method. It works wonders.

The same code, but using a list comprehension instead of a generator, which I needed.

 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
Jul 15 '19 at 21:01
source share



All Articles