Background
In the last post, we saw what are closures in Python. In this post, we will see what are decorators in Python. Closures are heavily used by Decorators. Let's see how. (You can also check my youtube video).
Decorators in Python
As we know already everything in Python is an Object. A decorator is an object that is used to modify a function or a class. The function decorator takes a reference to the decorated function and returns a new function. It will internally call the actual decorated/referenced function.
You can also have a class as a decorator instead of a function. When we decorate a method with a class, that function becomes an instance method of that class.
In either case, the original code is not changed.
You can also have a class as a decorator instead of a function. When we decorate a method with a class, that function becomes an instance method of that class.
In either case, the original code is not changed.
Let us say we have a function that makes a remote API call and we want to monitor the start and end time for that API. You have a method as follows:
def make_api_call(*params): # Simulate API call time.sleep(3) print("Done")
I have just added a sleep of 3 seconds to simulate the actual API call. So now if we want to monitor the start and end time of this API, we can modify this function to print time before and after the API call, but it is not correct to modify existing functions. This is where the decorator comes into the picture.
When you execute this code it prints the following output.
Making API call with params ('param1', 'param2') at 2020-05-24 22:31:19.480543
Done
Finishing API call with params ('param1', 'param2') at 2020-05-24 22:31:22.484090
You can notice how we decorated our actual method make_api_call with a custom decorator monitor_performance. Also, you must have noticed our decorator function used a closure - another internal method called wrapper_func to actually monitor start and end time.
As I mentioned before the decorator can be used to modify the actual method and internally calls the actual method. In this case, before we call the actual method we print start and end time.
You would have also noticed that the parameters passed to make_api_call are automatically passed to our wrapper function as we are returning this wrapper function from the decorator. Also, notice how we have declared decorator for our function using '@' notation.
Notes:
from datetime import datetime import time def monitor_performance(func): def wrapper_func(*params): print("Making API call with params {} at {}".format(params, datetime.now())) func(params) print("Finishing API call with params {} at {}".format(params, datetime.now())) return wrapper_func @monitor_performance def make_api_call(*params): # Simulate API call time.sleep(3) print("Done") make_api_call("param1", "param2")
When you execute this code it prints the following output.
Making API call with params ('param1', 'param2') at 2020-05-24 22:31:19.480543
Done
Finishing API call with params ('param1', 'param2') at 2020-05-24 22:31:22.484090
You can notice how we decorated our actual method make_api_call with a custom decorator monitor_performance. Also, you must have noticed our decorator function used a closure - another internal method called wrapper_func to actually monitor start and end time.
As I mentioned before the decorator can be used to modify the actual method and internally calls the actual method. In this case, before we call the actual method we print start and end time.
You would have also noticed that the parameters passed to make_api_call are automatically passed to our wrapper function as we are returning this wrapper function from the decorator. Also, notice how we have declared decorator for our function using '@' notation.
Using class instead of function for a decorator
The same code can be used with a class as a decorator. As we already know everything in Python is an object and it is callable if it defines the __call__() method.
from datetime import datetime import time class monitor_performace: def __init__(self, actual_func): self.actual_func = actual_func def __call__(self, *params): print("Making API call with params {} at {}".format(params, datetime.now())) self.actual_func(params) print("Finishing API call with params {} at {}".format(params, datetime.now())) @monitor_performace def make_api_call(*params): # Simulate API call time.sleep(3) print("Done") make_api_call("param1", "param2")
It prints similar output as before:
Making API call with params ('param1', 'param2') at 2020-05-24 22:39:22.895176
Done
Finishing API call with params ('param1', 'param2') at 2020-05-24 22:39:25.896923
Making API call with params ('param1', 'param2') at 2020-05-24 22:39:22.895176
Done
Finishing API call with params ('param1', 'param2') at 2020-05-24 22:39:25.896923
Notes:
- You can also decorate a class with another class
- You can chain decorated as well. Each decorated function/class will be called serially.