sdk sdk - 1 year ago 47
Python Question

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?

Answer Source

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