My misinterpretation of Python decorators
DrProject is written in Python, but I had only a little experience with Python when I first started. So I had to learn many things along the way, sometimes in the hard way.
Today’s topic is the semantics of decorators in Python (PEP 318). I used to think I knew how they worked, but I was wrong. Let’s start with a simple example:
def decorate(func):
def decorated(self):
print "Before"
func(self)
print "After"
return decorated
class Foo(object):
@decorate
def bar(self):
print "Hello, world!"
This is equivalent to the following code in the old style (I won’t repeat decorate because it remains unchanged):
class Foo(object):
def bar(self):
print "Hello, world!"
bar = decorate(bar)
In either case (they’re equivalent), if we create a Foo and invoke bar, we get:
Before Hello, world! After
So far so good? This is the kind of code I found in DrProject:
def action(name, template=None, stylesheets=None):
def _decorate(func):
def _execute(controller, req):
(...)
(...)
return _execute
(...)
return _decorate
class AdminController(Controller):
@action('list_projects',
template='admin_list_projects.html',
stylesheets=['admin.css'])
def _process_list_projects(self, req, order='name', desc=''):
(... omitted ...)
Somehow, I naïvely expected the decorator invocation to be equivalent to something like _process_list_projects = action(_process_list_projects, 'list_projects', template='admin_list_projects.html', stylesheets=['admin.css']), with the function as the zeroth argument. But that would be impossible, because action only takes 3 arguments, and has no provisions for extra arguments – so Python would err loudly if action is called with 4 arguments.
The key to understanding this situation lies in PEP 318, but the code itself is very suggestive too. Essentially, _process_list_projects is decorated by the function returned by action('list_projects', template=..., stylesheets=...). (See how action returns a nested _decorate function?) So overall, the explicit form of this decoration would be as follows:
class AdminController(Controller):
def _process_list_projects(self, req, order='name', desc=''):
(... omitted ...)
_process_list_projects = action('list_projects',
template='admin_list_projects.html',
stylesheets=['admin.css'])(_process_list_projects)
Notice the double call, i.e. action(...)(...).
To recap, a decorator usage like
@decorate
def foo():
(...)
expands to the form
def foo():
(...)
foo = decorate(foo)
where decorate is an arbitrary expression that evaluates to a decorator, which can be a simple function variable or the result of a function maker (e.g. make_decorator(1,2,3)).
I have nothing against the decorator syntax; in fact, I think it makes a lot of sense now that I understand it better. I just don’t want anyone else to repeat the same naïve misinterpretation.
2008-06-23-Mon at 09:50
That PEP is kind of difficult to grok. I remember I had to read it about 10 times to figure out exactly how to write my redecorator, which makes all decoration explicit.
I don’t hate decorators as much nowadays, though I still think they’re overussed.
2008-06-23-Mon at 13:00
Yup, decorators are silly :(
I got confused by the same thing the other day… It’s not fun.