Ian Bicking: the old part of his blog

Explaining Decorators

So, for the last ChiPy meeting I gave a presentation on py.test. I converted the Dive Into Python unittest example. Along the way I found what I thought was a good use of decorators to provide multiple parameters for a testing function. Without decorators this is what it would look like:

known_values = [(1, 'i'), (2, 'ii'), ...]
def test_to_roman():
    for integer, numeral in known_values:
        yield real_test_to_roman, integer, numeral
def real_test_to_roman(integer, numeral):
    assert roman.toRoman(integer) == numeral

That's not bad (easier than unittest, to be sure), but here's the decorator I made, for equivalent effect:

def param(param_list):
    def decorator(func):
        def yielder():
            for params in param_list:
                yield (func,) + params
        return yielder
    return decorator

def test_to_roman(integer, numeral):
    assert roman.toRoman(integer) == numeral

I thought, oh! how clever I am to come up with this example of decorators just when Python 2.4 is coming out, and to tie it into the month's topic (testing). I expected everyone to say my, how easy and useful these decorators are!

I didn't get quite that reaction. And I must admit, when people weren't sure what this did, my explanation made it clear this wasn't going to be immediately intuitive to people. This is more or less the explanation I gave:

The decorator line -- the line with the at sign -- is an expression that is evaluated. In this case the expression returns a function. Then it gets called with the function, and returns a generator function, and that generator function is what the name of the original function is bound to. Ta-da!

I actually went on longer than that, hoping that by noting that the original function is still around (bound to func) and describing the order of execution and a couple other things that it would magically become clear. It probably only made it worse.

I hope with a little thought and visual inspection that people can figure out the param decorator -- I think it will be a common pattern for decorators. Maybe the what's new document will be easier to understand. There's a real benefit to writing your own decorators; this particular example is only useful for tests, and only for py.test tests at that. I expect other domains to call for other decorators -- indeed, we are seeing a lot of clever uses of them in the Python Cookbook. They don't provide any new functionality, but they make a certain pattern feel much nicer.

But when it comes to explaning a decorator, it's clearly difficult. This was the same problem I've had explaining WSGI, which also involves lots of functions that return functions and call functions and whatnot. I think coming up with better names than "a function" will be important. In this case, emphasizing that param builds a decorator, which we call "decorator" (not "a function that takes a function and returns a function"). And maybe instead of "yielder" it should be "replacement_func" or something. I'm finding the use of language becomes very important when dealing with higher-order functions.

Update: There was quite a bit of discussion on the ChiPy mailing list about this, and perhaps some resolution and understanding. You can start reading the thread here. Adrian Holavaty wrote a good example of decorators as well.

Created 16 Dec '04
Modified 09 Jan '05