Decorator Factories (and decorator factory factories)

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.

Decorators


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, timestwo is now actually the closure inner, which keeps a reference to the original timestwo in 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.

Decorator Factories


So let's say you want a more flexible decorator that can raise to any power, not just 2. We could individually define squared(), cubed(), 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 decorator
Since 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*2
This 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):
    ...
GET itself is just a decorator factory, but I defined GET (and POST, and PUT, and 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.

2 comments

Post a Comment