Decorators are a simple, handy way of implementing certain features in any language, but many people find them perplexing. When you start talking about functions that generate decorators, it gets a little twisty; when you then make helpful functions to create functions that create decorators, you might have to stop for a minute to unclench your brain. Really, though, the concept is quite simple if you take it step by step, so here it is (after the jump), hopefully demystified.
For the uninitiated, a decorator is a function that replaces another function, adding extra functionality along the way. Here is a trivial example in Python:
def squared(f): def inner(*args): result = f(*args) return result**2 return inner def timestwo(n): return n*2 timestwo = squared(timestwo)
squared()accepts a function and returns a function, which is the fundamental characteristic of a decorator. We then decorate
timestwo()by passing it through the decorator and replacing the reference to the original function with the result. After all this has executed,
timestwois now actually the closure
inner, which keeps a reference to the original
timestwoin the decorator's scope so it can call it and mess with the result.
In Python you can use nice decorator syntax:
@squared def timestwo(n): ...Which is synonymous with the manual call to the decorator and reassignment.
So let's say you want a more flexible decorator that can raise to any power, not just 2. We could individually define
timed()and so on, but it's much easier just to create a function that returns the decorator we need. We'll make a decorator factory,
raisetopower, that returns a decorator,
decorator, that returns a function,
inner, which will replace the decorated function. Again, we'll use closures all the way down, so we keep all the information we need in scope:
def raisetopower(exponent): def decorator(f): def inner(*args): result = f(*args) return result**exponent return inner return decoratorSince
raisetopower(2)will return what is essentially
squared()from above, we could do:
squared = raisetopower(2) cubed = raisetopower(3)But it is much easier just to do:
@raisetopower(2) def timestwo(n): return n*2This may look strange to some, but bear in mind that it is functionally identical to:
timestwo = raisetopower(2)(timestwo)And since
raisetopower(2)==squared, we've recreated the same situation we had above, only we can now pass in any exponent we want.
Decorator Factory Factories
Naturally the same technique, creating and returning a closure wrapping the original function, can be nested indefinitely; at some point it becomes unreadable. Personally, I would never have multiple calls on the same line, so I draw the line at actually calling decorator factories. I have, however, found uses for decorator factory factories, which you would need when you wanted a reusable decorator factory with some information frozen inside. For example, in txrestapi, I needed decorators that tied a function to both a request method and a regular expression, like:
@GET('^/.*$') def onGetAnything(self): ...
GETitself is just a decorator factory, but I defined
DELETE) using a decorator factory factory:
def decorator_factory_factory(method): def decorator_factory(regex): def decorator(f): def inner(*args, **kwargs): # do stuff with f(*args, **kwargs) # and method and regex return inner return decorator return decorator_factory GET = decorator_factory_factory('GET') PUT = decorator_factory_factory('PUT')Now, I could have used the function to do:
@decorator_factory_factory('GET')('^/.*$') def onGetAnything(self): ...But as I said above, you reach a point where it's totally unreadable, so draw the line appropriately.
So that's that: decorators, and functions that create decorators, and functions that create functions that create decorators, all unpacked. Really it's just the same technique, replacing a function with a closure wrapping that function, over and over.