Python decorator implements the DRY (no repeated code) principle, pythondry
The Python decorator is a powerful tool for eliminating redundancy. As you modularize a function into an appropriate size method, the decorator Can make it a concise function even for the most complex workflow.
For example, let's take a look at the Django web framework. This framework receives a method object for processing requests and returns a response object:
def handle_request(request): return HttpResponse("Hello, World")
In a recent case, I need to write several api methods that meet the following conditions:
- Return json response
- If the request is a GET request, the error code is returned.
As an example of registering an api endpoint, I will write it like this:
def register(request): result = None # check for post only if request.method != 'POST': result = {"error": "this method only accepts posts!"} else: try: user = User.objects.create_user(request.POST['username'], request.POST['email'], request.POST['password']) # optional fields for field in ['first_name', 'last_name']: if field in request.POST: setattr(user, field, request.POST[field]) user.save() result = {"success": True} except KeyError as e: result = {"error": str(e) } response = HttpResponse(json.dumps(result)) if "error" in result: response.status_code = 500 return response
However, I will write the json response and error return code in each api method. This will lead to a large number of logical duplicates. So let's try to use the decorator to implement the DRY principle.
Decorator Introduction
If you are not familiar with the decorator, I can explain it briefly. In fact, the decorator is an effective function package. When the python interpreter loads a function, it will execute the package, the package can modify the receiving parameters and return values of the function. For example, if I want to always return an integer greater than the actual return value, I can write the decorator as follows:
# a decorator receives the method it's wrapping as a variable 'f'def increment(f): # we use arbitrary args and keywords to # ensure we grab all the input arguments. def wrapped_f(*args, **kw): # note we call f against the variables passed into the wrapper, # and cast the result to an int and increment . return int(f(*args, **kw)) + 1 return wrapped_f # the wrapped function gets returned.
Now we can use the @ symbol to decorate another function:
@incrementdef plus(a, b): return a + b result = plus(4, 6)assert(result == 11, "We wrote our decorator wrong!")
The decorator modifies the existing function and assigns the result returned by the decorator to the variable. In this example, the result of 'plus 'actually points to the result of increment (plus.
Error returned for non-post requests
Now let's apply decorator in some more useful scenarios. If the POST request is not received in django, an error response is returned using the decorator.
def post_only(f): """ Ensures a method is post only """ def wrapped_f(request): if request.method != "POST": response = HttpResponse(json.dumps( {"error": "this method only accepts posts!"})) response.status_code = 500 return response return f(request) return wrapped_f
Now we can apply this modifier in the above registration api:
@post_onlydef register(request): result = None try: user = User.objects.create_user(request.POST['username'], request.POST['email'], request.POST['password']) # optional fields for field in ['first_name', 'last_name']: if field in request.POST: setattr(user, field, request.POST[field]) user.save() result = {"success": True} except KeyError as e: result = {"error": str(e) } response = HttpResponse(json.dumps(result)) if "error" in result: response.status_code = 500 return response
Now we have a decorator that can be reused in each api method.
Send json response
To send a json response (processing status code 500 at the same time), we can create another decorator:
def json_response(f): """ Return the response as json, and return a 500 error code if an error exists """ def wrapped(*args, **kwargs): result = f(*args, **kwargs) response = HttpResponse(json.dumps(result)) if type(result) == dict and 'error' in result: response.status_code = 500 return response
Now we can remove json-related code in the original method and add a decorator as a replacement:
@post_only@json_responsedef register(request): try: user = User.objects.create_user(request.POST['username'], request.POST['email'], request.POST['password']) # optional fields for field in ['first_name', 'last_name']: if field in request.POST: setattr(user, field, request.POST[field]) user.save() return {"success": True} except KeyError as e: return {"error": str(e) }
Now, if I need to write a new method, I can use the decorator for redundancy. If I want to write the logon method, I only need to write the code with positive correlation:
@post_only@json_responsedef login(request): if request.user is not None: return {"error": "User is already authenticated!"} user = auth.authenticate(request.POST['username'], request.POST['password']) if user is not None: if not user.is_active: return {"error": "User is inactive"} auth.login(request, user) return {"success": True, "id": user.pk} else: return {"error": "User does not exist with those credentials"}
BONUS: parameterize your request Method
I have used the Tubogears framework, and I like it very much when request parameters are directly interpreted and transferred to methods. So how can we imitate this feature in Django? Well, the decorator is a solution!
For example:
def parameterize_request(types=("POST",)): """ Parameterize the request instead of parsing the request directly. Only the types specified will be added to the query parameters. e.g. convert a=test
Note that this is an example of a parameterized modifier. In this example, the result of the function is the actual decorator.
Now I can write the method with the parameterized decorator! I can even choose whether to allow GET and POST, or only one request parameter type.
@post_only@json_response@parameterize_request(["POST"])def register(request, username, email, password, first_name=None, last_name=None): user = User.objects.create_user(username, email, password) user.first_name=first_name user.last_name=last_name user.save() return {"success": True}
Now we have a simple and easy-to-understand api.
BONUS #2: Use functools. wraps to save docstrings and function names
Unfortunately, one side effect of using the decorator is that the method name (name) and docstring (doc) values are not saved:
def increment(f): """ Increment a function result """ wrapped_f(a, b): return f(a, b) + 1 return wrapped_f@incrementdef plus(a, b) """ Add two things together """ return a + bplus.__name__ # this is now 'wrapped_f' instead of 'plus'plus.__doc__ # this now returns 'Increment a function result' instead of 'Add two things together'
This will cause trouble for applications that use reflection, such as Sphinx, an application that automatically generates documents.
To solve this problem, we can append the name and docstring with the 'wraps' modifier:
from functools import wrapsdef increment(f): """ Increment a function result """ @wraps(f) wrapped_f(a, b): return f(a, b) + 1 return wrapped_f@incrementdef plus(a, b) """ Add two things together """ return a + b plus.__name__ # this returns 'plus'plus.__doc__ # this returns 'Add two things together'
BONUS #3: decorator
If you take a closer look at the above method of using the decorator, there are many duplicates in the package declaration and return.
You can installpython egg ‘decorator'
The decorator contains a decorator template!
Use easy_install:
$ sudo easy_install decorator
Or Pip:
$ pip install decorator
Then you can write the following code:
from decorator import decorator@decoratordef post_only(f, request): """ Ensures a method is post only """ if request.method != "POST": response = HttpResponse(json.dumps( {"error": "this method only accepts posts!"})) response.status_code = 500 return response return f(request)
This decorator saves the return values of name and doc, that is, it encapsulates
Functools. wraps!