Forget functions: A class based approach to Python decorators

A decorator is simple syntactic sugar for applying a transformation to a function or method. Both

class A(object):
    @decorator
    def name(self):
        return "foo"

and

class A(object):
    def name(self):
        return "foo"
    name = decorator(name)

are equivalent. The quintessential decorator example contains a function returning a closure that performs some transformation on the wrapped function’s return value. For example

def upper(wrapped):
    def shim(self):
        # convert returned value to upper case
        return wrapped(self).upper()
    return shim
 
class A(object):
    @upper
    def name(self):
        return "foo"
 
>>> a = A()
>>> print a.name()
FOO

When you wish to create a decorator that accepts additional arguments such as

class A(object):
    @expose("get_name")
    def name(self):
        return "foo"

the hierarchy of closures can become complicated, and in my opinion unpythonic.

Class based decorators with arguments

Since a decorator can be any callable and we can override a class’s __call__ method to emulate a function call, a “decorator class” can be constructed. This is nice because in the _shim we have access to both the decorator class instance (self) and the instance of the class containing the decorated method (instance).

class expose(object):
    def __init__(self, name):
        self._name = name
        self._wrapped = None
 
    def _shim(self, instance, *args, **kwargs):
        print 'Special RPC handling for %s, %r, %r' % \
            (self._name, args, kwargs)
        return self._wrapped(instance, *args, **kwargs)
 
    def __call__(self, wrapped):
        self._wrapped = wrapped
        def shim(instance, *args, **kwargs):
            return self._shim(instance, *args, **kwargs)
        return shim
 
class A(object):
    @expose("get_name")
    def name(self):
        return "foo"
 
>>> a = A()
>>> print a.name()
Special RPC handling for get_name, (), {}
foo

Class based decorators without arguments

Expose handles the case where the decorator accepts arguments. But what if we just wanted to write

class A(object):
    @expose
    def name(self):
        return "foo"

Since expose is now a class, its __init__ method will be called with the method name as an argument. We cannot simply implement a __call__ method on expose because we want to get hold of the decorated method’s containing class instance. The trick here is to change expose into a non-data descriptor by adding a __get__ special method. This __get__ is passed the decorator’s containing class instance, thus we create and return a closure that calls expose’s _shim method with the desired arguments.

class expose(object):
    def __init__(self, wrapped):
        self._wrapped = wrapped
 
    def _shim(self, instance, *args, **kwargs):
        print 'Special RPC handling for %s, %r, %r' % \
            (self._wrapped.__name__, args, kwargs)
        return self._wrapped(instance, *args, **kwargs)
 
    def __get__(self, instance, owner):
        def shim(*args, **kwargs):
            return self._shim(instance, *args, **kwargs)
        return shim
 
class A(object):
    @expose
    def name(self):
        return "foo"
 
>>> a = A()
>>> print a.name()
Special RPC handling for name, (), {}
foo

Generic class based decorator

We can combine the two methods above to create a generic decorator base class that handles both the argument and no argument case. The implementation of decorator_base can be found here. Below is an example where the example decorator class extends decorator_base.

class example(decorator_base):
    default_positional_args = ('Default Name',)
 
    def _arg_init(self,
                  name,
                  description="Default Description"):
        self._name = name
        self._description = description
 
    def _shim(self, instance, *args, **kwargs):
        print self._name, self._description, args, kwargs
        return self._wrapped(instance, *args, **kwargs)
 
class Test(object):
    @example
    def upper(self, text):
        return text.upper()
 
    @example('Verbose Name', description='Verbose Description')
    def lower(self, text):
        return text.lower()
 
>>> t = Test()
>>> print t.upper('hElLo')
Default Name Default Description ('hEllo',) {}
HELLO
>>> print t.lower('HeLlO')
Verbose Name Verbose Description ('HeLlO',) {}
hello

An equivalent class based decorator can be constructed using metaclasses, this will be the subject of further post. I hope this post gives sheds some light on using classes to implement decorators.

Bookmark and Share

One Comment

  1. Lora Ireland says:

    This is great! Thanks for this blog. I am starting development and this was a big help.

Leave a Reply