Why You Should Use Python Decorator

<span>Photo by <a href=”https://unsplash.com/@wsheng1011?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCo
<span>Photo by <a href=”https://unsplash.com/@wsheng1011?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCo
Photo by Han Wei Sheng on Unsplash

Introduction

Basic Syntax

add_log — to add log to inspect all the positional and keyword arguments of a function before actually calling it

send_email — to accept some positional and keyword arguments for sending out emails

def add_log(func):
def log(*args, **kwargs):
for arg in args:
print(f"{func.__name__} - args: {arg}")
for key, val in kwargs.items():
print(f"{func.__name__} - {key}, {val}")

return func(*args, **kwargs)
return log
def send_email(subject, to, **kwargs):
#send email logic
print(f"email sent to {to} with subject {subject}.")

We can pass in the send_email function to add_log as argument, and then we trigger the sending of the email.

sender = add_log(send_email) 
sender("hello", "contact@codeforests.com", attachment="debug.log", urgent_flag=True)

This code will generate the output as per below:

Image for post
Image for post

You can see that the send_email function has been invoked successfully after all the arguments were printed out. This is exactly what decorator is doing — extending the functionality of the send_email function without changing its original structure. When you directly call the send_email again, you can still see it’s original behavior without any change.

Image for post
Image for post

Python decorator as a function

So let implement our own decorator with @ syntax.

Assuming we have the below decorator function and we want to check if user is in the whitelist before allowing he/she to access certain resources. We follow the Python convention to use wrapper as the name of the inner function (although it is free of your choice to use any name).

class PermissionDenied(Exception): 
pass
def permission_required(func):
whitelist = ["John", "Jane", "Joe"]
def wrapper(*args, **kwargs):
user = args[0]
if not user in whitelist:
raise PermissionDenied
func(*args, **kwargs)

return wrapper

Next, we decorate our function with permission_required as per below:

@permission_required 
def read_file(user, file_path):
with open(file_path, "r") as f:
#print out the first line of the file
print(f.readline())

When we call our function as per normal, we shall expect the decorator function to be executed first to check if user is in the whitelist.

read_file("John", r"C:\pwd.txt")

You can see the below output has been printed out:

Image for post
Image for post

If we pass in some user name not in the whitelist:

read_file("Johnny", r"C:\pwd.txt")

You would see the permission denied exception raised which shows everything works perfect as per we expected.

Image for post
Image for post

But if you are careful enough, you may find something strange when you check the below.

Image for post
Image for post

So it seems there is some flaw with this implementation although the functional requirement has been met. The function signature has been overwritten by the decorator, and this may cause some confusing to other people when they want to use your function.

Use of the functools.wraps

Let update our decorator function again by adding @wraps(func) to the wrapper function:

from functools import wraps 
def permission_required(func):
...
@wraps(func)
def wrapper(*args, **kwargs):
...
return wrapper

Finally, when we check the function signature and name again, it shows the correct information now.

Image for post
Image for post

So what happened was that, the @wraps(func) would invoke a update_wrapper function which updates the metadata of the original function automatically so that you will not see the wrapper’s metadata. You may want to check the update_wrapper function in the functools module to further understand how the metadata is updated.

Beside decorating normal function, the decorator function can be also used to decorate the class function, for instance, the @staticmethod and @property are commonly seen in Python code to decorate the class functions.

Python decorator as a class

Below is the code that implements our earlier example as a class:

from functools import update_wrapper class PermissionRequired: 
def __init__(self, func):
self._whitelist = ["John", "Jane", "Joe"]
update_wrapper(self, func)
self._func = func
def __call__(self, *args, **kwargs):
user = args[0]
if not user in self._whitelist:
raise PermissionDenied
return self._func(*args, **kwargs)

Take note that we will need to call the update_wrapper function to manually update the metadata for our decorated function. And same as before, we can continue using @ with class name to decorate our function.

@PermissionRequired def read_file(user, file_path): 
with open(file_path, "r") as f:
#print out the first line of the file
print(f.readline())

Conclusion

Originally published at https://www.codeforests.com on August 7, 2020.

Written by

Resources and tutorials for python, data science and automation solutions

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store