Python decorators are a powerful tool to remove redundancy. Along with modularizing functionality into appropriate bite-sized methods, it makes even the most complex workflows into concise functionality.
For example, let’s look at the Django web framework, which handles requests by methods which receive a method object and return a response object:
1 | def handle_request(request): |
A case I ran into recently was having to write several api methods which must:
- return json responses some must return an error code if it’s a GET
- request vs a POST
As an example, for a register api endpoint, I would write something like this:
1 | def register(request): |
However, I’m going to need json responses and error returned in pretty much every api method I create. This would result in a majority of logic reproduced over and over again. Let’s try implementing some DRY principles with decorators.
Decorator Introduction
If you’re not familiar with decorators, they are effectively function wrappers that are run when the python interpreter loads the function, and can modify what the function receives and returns. For example, if I wanted to always return an integer result of one larger than whatever was returned, I could write my decorator as so:
1 | # a decorator receives the method it's wrapping as a variable 'f' |
And now we can use it to decorate another method using the ‘@’ symbol:
1 |
|
Decorators modify the existing function, and assign the variable to whatever is returned by the decorator. In this case, ‘plus’ really refers to the result of increment(plus).
Return an error on non-post requests
Now let’s apply decorators to something useful. Let’s make a decorator that returns an error response if the request received isn’t a POST request in django:
1 | def post_only(f): |
Now, we can apply this to our register api above:
1 |
|
And now we have a repeatable decorator we can apply to every api method we have.
Send the response as json
To send the response as json (and also handle the 500 status code while we’re at it), we can just create another decorator:
1 | def json_response(f): |
Now we can remove the json code from our methods, and add a decorator instead:
1 |
|
Now, if I need to write a new method, I can just use these decorators to re-do the redundant work. If I need to make a sign-in method, I only have to write the real relevant code a second time:
1 |
|
BONUS: parameterizing your request method
I’ve used the Turbogears framework for python, and something I’ve fallen in love with is the way query parameters are interpreted and passed directory into the method. So how can I mimic this behaviour in Django? Well, a decorator is one way!
Here’s one:
1 | def parameterize_request(types=("POST",)): |
Note that this is an example of a parameterized decorator. In this case, the result of the function is the actual decorator.
Now, I can write my methods with parameterized arguments! I can even choose whether to allow GET and POST, or just one type of query parameter.
1 |
|
Now, we have a succinct, and easily understandable api!
BONUS 2: Using functools.wraps to preserve docstrings and function name
(Credit goes to Wes Turner to pointing this out)
Unfortunately, one of the side effects of using decorators is that the method’s name (name) and docstring (doc) values are not preserved:
1 | def increment(f): |
This causes issues for applications which use reflection, like Sphinx, a library to that automatically generates documentation for your python code.
To resolve this, you can use a ‘wraps’ decorator to attach the name and docstring:
1 |
|
BONUS 3: Using the ‘decorator’ decorator
(Credit goes to LeszekSwirski for this awesome tip.)
NOTE: Elghinn mentions in the comments that there are caveats
to using this decorator.
If you look at the way decorators above, there is a lost of repeating going on there as well, in the declaring and returning of a wrapper.
you can install the python egg ‘decorator’, which includes a ‘decorator’ decorator that provides the decorator boilerplate for you!
With easy_install:
1 | $ sudo easy_install decorator |
Or Pip:
1 | $ pip install decorator |
Then you can simply write:
1 | from decorator import decorator |
What’s even more awesome about this decorator, is the fact that it preserves the return values of name and doc, so it wraps in the functionality functools.wraps performs as well!