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 @param(known_values) 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.