A decorator for decorators that accept additional arguments

February 28, 2013

Once you're happy with functions returning other functions (technically closures), basic ordinary decorators in Python are easy enough to use, write, and understand. You get things like:

from functools import wraps
def trace(func):
    @wraps(func)
    def _d(*args, **kwargs):
        print "start", func.__name__
        try:
            return func(*args, **kwargs)
        finally:
            print "end", func.__name__
    return _d

@trace
def jim(a, b):
    print "in jim"
    return a + b

(If you're decorating functions that all have the same argument signature, you can make the _d() closure take the specific arguments and pass them on instead of using the *args and **kwargs approach.)

But sometimes you have decorators that want to take additional arguments (over and above the function that they get handed to decorate). The syntax for doing this when you're declaring functions that will get decorated is easy and obvious:

@tracer("barney")
def fred(a, b, c):
    print "in fred:", a
    return b - c

Unfortunately the niceness stops there; an implementation of tracer() is much more complicated than trace(). Because of how Python has defined things, tracer() is no longer a decorator but a decorator factory, something that when called creates and returns the decorator that will actually be applied to fred(). You wind up with functions returning functions that return functions, or with tracer() actually being a class (so that calling it creates an instance that when called will actually do the decorating).

What we would like is for tracer() to be a regular decorator that just has extra arguments. Well, we can do that; all we need is another decorator that we will use on tracer() itself. Like so:

from functools import partial
def decor(decorator):
    @wraps(decorator)
    def _dd(*args, **kwargs):
        return partial(decorator, *args, **kwargs)
    return _dd

@decor
def tracer(note, func):
    fname = func.__name__
    @wraps(func)
    def _d(*args, **kwargs):
        print "-> %s: start %s" % (note, fname)
        try:
            return func(*args, **kwargs)
        finally:
            print "-> %s: end %s" % (note, fname)
    return _d

(You can make tracer()'s arguments have the function to be decorated first, but then you have to do more work because you can't use functools.partial(). While I think that func belongs as the first argument, I don't quite feel strongly enough to give up partial().)

The one nit with this is that positional arguments really are positional and keyword arguments really are keywords. You can't, for example, write:

@tracer(note="greenlet")
def fred(....):
    ....

(The only way around this is changing the order of arguments to tracer() so that func is first, which means giving up the convenience of just using partial().)

I've been thinking about decorators lately (they're probably the right solution for a code structure problem) and had this issue come up in my tentative design, so I felt like writing down my solution for later use. I'm sure that regular users of decorators already know all of these tricks.

(Decorators are one of the Python things I should use more. I don't for complex reasons that involve my history with significant Python coding.)

Written on 28 February 2013.
« Looking at whether Zen-listed IPs keep trying to send us email
The mythology of spending money on things, or not doing so »

Page tools: View Source, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Thu Feb 28 23:25:57 2013
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.