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.
This is great! Thanks for this blog. I am starting development and this was a big help.