How to get all methods of a Python class with given decorator?

Question:

How to get all methods of a given class A that are decorated with the @decorator2?

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

    @decorator1
    def method_b(self, b):
      pass

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

||

Answers:

Method 1: Basic registering decorator

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


Method 2: Sourcecode parsing

If you do not have control over the class definition, which is one interpretation of what you’d like to suppose, this is impossible (without code-reading-reflection), since for example the decorator could be a no-op decorator (like in my linked example) that merely returns the function unmodified. (Nevertheless if you allow yourself to wrap/redefine the decorators, see Method 3: Converting decorators to be “self-aware”, then you will find an elegant solution)

It is a terrible terrible hack, but you could use the inspect module to read the sourcecode itself, and parse it. This will not work in an interactive interpreter, because the inspect module will refuse to give sourcecode in interactive mode. 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 one has to pay attention to parsing and the python syntax, e.g. @deco and @deco(... are valid results, but @deco2 should not be returned if we merely ask for 'deco'. We notice that according to the official python syntax at http://docs.python.org/reference/compound_stmts.html decorators are as follows:

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

We breathe a sigh of relief at not having to deal with cases like @(deco). But note that this still doesn’t really help you if you have really really complicated decorators, such as @getDecorator(...), e.g.

def getDecorator():
    return deco

Thus, this best-that-you-can-do strategy of parsing code cannot detect cases like this. Though if you are using this method, what you’re really after is what is written on top of the method in the definition, which in this case is getDecorator.

According to the spec, it is also valid to have @foo1.bar2.baz3(...) as a decorator. You can extend this method to work with that. You might also be able to extend this method to return a <function object ...> rather than the function’s name, with lots of effort. This method however is hackish and terrible.


Method 3: Converting decorators to be “self-aware”

If you do not have control over the decorator definition (which is another interpretation of what you’d like), then all these issues go away because you have control over how the decorator is applied. Thus, you can modify the decorator by wrapping it, to create your own decorator, and use that to decorate your functions. Let me say that yet again: you can make a decorator that decorates the decorator you have no control over, “enlightening” it, which in our case makes it do what it was doing before but also append a .decorator metadata property to the callable it returns, allowing you to keep track of “was this function decorated or not? let’s check function.decorator!”. And then you can iterate over the methods of the class, and just check to see if the decorator has the appropriate .decorator property! =) As demonstrated 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, 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 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, a “registered decorator” must be the outermost decorator, otherwise the .decorator attribute annotation will be lost. For example in a train of

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

you can only see metadata that decoOutermost exposes, unless we keep references to “more-inner” wrappers.

sidenote: the above method can also build up a .decorator that keeps track of the entire stack of applied decorators and input functions and decorator-factory arguments. =) For example if you consider the commented-out line R.original = func, it is feasible to use a method like this to keep track of all wrapper layers. This is personally what I’d do if I wrote a decorator library, because it allows for deep introspection.

There is also a difference between @foo and @bar(...). While they are both “decorator expressons” as defined in the spec, note that foo is a decorator, while bar(...) returns a dynamically-created decorator, which is then applied. Thus you’d need a separate function makeRegisteringDecoratorFactory, that is somewhat like 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

Demonstration for @decorator(...):

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

deco2 = makeRegisteringDecoratorFactory(deco2)

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

@deco2()
def f():
    pass

This generator-factory wrapper also works:

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

bonus Let’s even try the following with Method #3:

def getDecorator(): # let's 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 even though it was never explicitly written in the class. Unlike method2, this will also work if the method is added at runtime (manually, via a metaclass, etc.) or inherited.

Be aware that you can also decorate a class, so if you “enlighten” a decorator that is used to both decorate methods and classes, and then write a class within the body of the class you want to analyze, then methodsWithDecorator will return decorated classes as well as decorated methods. One could consider this a feature, but you can easily write logic to ignore those by examining the argument to the decorator, i.e. .original, to achieve the desired semantics.

Answered By: ninjagecko

Maybe, if the decorators are not too complex (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
Answered By: user227667

To expand upon @ninjagecko’s excellent answer in Method 2: Source code parsing, you can use the ast module introduced in Python 2.6 to perform self-inspection as long as the inspect module 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:

@x.y.decorator2
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())"]}
Answered By: Shane Holloway

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

e.g.

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

now every function with the deco decorator will be added to functions.

Answered By: Thomas King

I don’t want to add much, just a simple variation of ninjagecko’s Method 2. It works wonders.

Same code, but using list comprehension instead of a generator, which is what 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]
Answered By: Skovborg Jensen

If you do have control over the decorators, you can use decorator classes rather than functions:

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>}
Answered By: Jason S

For those of us who just want the absolute simplest possible case – namely, a single-file solution where we have total control over both the class we’re working with and the decorator we’re trying to track, I’ve got an answer. ninjagecko linked to a solution for when you have control over the decorator you want to track, but I personally found it to be complicated and really hard to understand, possibly because I’ve never worked with decorators until now. So, I’ve created the following example, with the goal of being as straightforward and simple as possible. It’s a decorator, a class with several decorated methods, and code to retrieve+run all methods that have a specific decorator applied to them.

# our decorator
def cool(func, *args, **kwargs):
    def decorated_func(*args, **kwargs):
        print("cool pre-function decorator tasks here.")
        return_value = func(*args, **kwargs)
        print("cool post-function decorator tasks here.")
        return return_value
    # add is_cool property to function so that we can check for its existence later
    decorated_func.is_cool = True
    return decorated_func

# our class, in which we will use the decorator
class MyClass:
    def __init__(self, name):
        self.name = name

    # this method isn't decorated with the cool decorator, so it won't show up 
    # when we retrieve all the cool methods
    def do_something_boring(self, task):
        print(f"{self.name} does {task}")
    
    @cool
    # thanks to *args and **kwargs, the decorator properly passes method parameters
    def say_catchphrase(self, *args, catchphrase="I'm so cool you could cook an egg on me.", **kwargs):
        print(f"{self.name} says "{catchphrase}"")

    @cool
    # the decorator also properly handles methods with return values
    def explode(self, *args, **kwargs):
        print(f"{self.name} explodes.")
        return 4

    def get_all_cool_methods(self):
        """Get all methods decorated with the "cool" decorator.
        """
        cool_methods =  {name: getattr(self, name)
                            # get all attributes, including methods, properties, and builtins
                            for name in dir(self)
                                # but we only want methods
                                if callable(getattr(self, name))
                                # and we don't need builtins
                                and not name.startswith("__")
                                # and we only want the cool methods
                                and hasattr(getattr(self, name), "is_cool")
        }
        return cool_methods

if __name__ == "__main__":
    jeff = MyClass(name="Jeff")
    cool_methods = jeff.get_all_cool_methods()    
    for method_name, cool_method in cool_methods.items():
        print(f"{method_name}: {cool_method} ...")
        # you can call the decorated methods you retrieved, just like normal,
        # but you don't need to reference the actual instance to do so
        return_value = cool_method()
        print(f"return value = {return_value}n")

Running the above example gives us the following output:

explode: <bound method cool.<locals>.decorated_func of <__main__.MyClass object at 0x00000220B3ACD430>> ...
cool pre-function decorator tasks here.
Jeff explodes.
cool post-function decorator tasks here.
return value = 4

say_catchphrase: <bound method cool.<locals>.decorated_func of <__main__.MyClass object at 0x00000220B3ACD430>> ...
cool pre-function decorator tasks here.
Jeff says "I'm so cool you could cook an egg on me."
cool post-function decorator tasks here.
return value = None

Note that the decorated methods in this example have different types of return values and different signatures, so the practical value of being able to retrieve and run them all is a bit dubious. However, in cases where there are many similar methods, all with the same signature and/or type of return value (like if you’re writing a connector to retrieve unnormalized data from one database, normalize it, and insert it into a second, normalized database, and you have a bunch similar methods, e.g. 15 read_and_normalize_table_X methods), being able to retrieve (and run) them all on the fly could be more useful.

Answered By: Nick Muise