Search code examples
pythonoopmetaprogrammingdecoratorpython-decorators

Decorators in class methods: compatibility with 'getattr'


I need to make wrappers for class methods, to be executed before and/or after the call of a specific method.

Here is a minimal example:

class MyClass:

    def call(self, name):
        print "Executing function:", name
        getattr(self, name)()

    def my_decorator(some_function):
        def wrapper():
            print("Before we call the function.")
            some_function()
            print("After we call the function.")
            return wrapper

    @my_decorator
    def my_function(self):
        print "My function is called here."


engine = MyClass()
engine.call('my_function')

This gives me an error at the line getattr(self, name)():

TypeError: 'NoneType' object is not callable

If I comment out the decorator before the class method, it works perfectly:

class MyClass:

    def call(self, name):
        print "Executing function:", name
        getattr(self, name)()

    def my_decorator(some_function):
        def wrapper():
            print("Before we call the function.")
            some_function()
            print("After we call the function.")
            return wrapper

    # @my_decorator
    def my_function(self):
        print "My function is called here."


engine = MyClass()
engine.call('my_function')

The output is:

Executing function: my_function

My function is called here.

The decorator itself is identical to textbook examples. It looks like something goes wrong at a low level when calling a decorated method in Python with getattr.

Do you have any ideas on how to fix this code?


Solution

  • This has nothing to do with getattr(). You get the exact same error when you try to call my_function() directly:

    >>> engine.my_function()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: 'NoneType' object is not callable
    

    You have 2 problems:

    • Your decorator never returns the wrapper, so None is returned instead. This return value replaces my_function and is the direct cause of your error; MyClass.my_function is set to None:

      >>> MyClass.my_function is None
      True
      
    • Your wrapper takes no arguments, including self. You'll need this for it to work once you do return it properly.

    The first problem is fixed by un-indenting the return wrapper line; it is currently part of the wrapper function itself, and should be part of my_decorator instead:

    def my_decorator(some_function):
        def wrapper(self):
            print("Before we call the function.")
            # some_function is no longer bound, so pass in `self` explicitly
            some_function(self)
            print("After we call the function.")
        # return the replacement function
        return wrapper