Understanding python decorators with step-by-step examples
Bharat Kalluri / 2020-10-27T15:27:47+05:30
Decorators are used to add functionality to an existing code. They allow you to wrap a function within another function, thereby adding functionality. Let us look at some examples to get a sense of what decorators are. Before that, let us get some concepts cleared out about functions in python
Functions are first class objects in python
A function can be used like a value in python. For example
def printer():
print("Printing")
a = printer
a()
# printing
Here we assigned the function to a variable and now the variable holds the function.
Functions can also be passed around as arguments between functions (since they just act like values). Another popular example is map
. The first argument of the function is a function (Callable
) and the second argument of the function is an Iterable
(List, tuple etc.). Then map
runs over the Iterable
and executes the function.
Let us write our own simple map
implementation
from typing import Callable, Iterable
def mapper(func: Callable, list_to_iterate: Iterable):
return [func(el) for el in list_to_iterate]
def double(number: int):
return number * 2
mapper(double, [1,2])
# [2, 4]
Functions can also be nested inside one another. For example
def outer_func():
def inner_func():
print("Inner func")
print("Outer func")
outer_func()
# Outer func
# Inner func
Armed with this information, let us create our first decorator
A simple decorator
Let us create a function that counts the number of seconds a function took to execute. A very similar example to the mapper
function.
from time import time, sleep
from typing import Callable
def timer_decorator(func: Callable) -> Callable:
def wrapper():
start_time = time()
func()
end_time = time()
print(end_time-start_time)
return wrapper
def sleeper() -> None:
sleep(5)
timed_sleeper: Callable = timer_decorator(sleeper)
timed_sleeper()
# 5.00
The above example pretty much combines everything we have learned up until now. The timer_decorator
returns a wrapper function. The function starts up a timer, runs the input function, and then ends the timer and calculates the difference to print it out. sleeper
just sleeps for 5 seconds.
When timer_decorator
is called with sleeper
as an argument. We get back a function which wraps the sleeper func with the time difference calculation logic. Now, if timed_sleep
is called, it runs and prints out the number of seconds it took to run the sleeper
func.
Takes some time and make sure you understand what is happening. This is what decorators are. Python gives us some syntactic sugar over the same idea. Using decorators syntax, the sleeper function would look like this
@timer_decorator
def sleeper():
sleep(5)
sleeper()
# 5.00
That is it! Decorators wrap a function and modify its functionality/behaviour
Decorating functions with arguments
The current code does not work well for functions with arguments, let us see why
from time import time, sleep
from typing import Callable
def timer_decorator(func: Callable) -> Callable:
def wrapper():
start_time = time()
func()
end_time = time()
print(end_time-start_time)
return wrapper
@timer_decorator
def sleeper(sec: int):
sleep(sec)
sleeper(5)
# ---> 16 sleeper(5)
# TypeError: wrapper() takes 0 positional arguments but 1 was given
The error is pretty clear, wrapper does not take any arguments. So now we should have a mechanism to cascade arguments to child functions. Python's *args
and **kwargs
come in handy here. Let us modify the decorator so that it cascades all arguments to child functions.
from time import time, sleep
from typing import Callable
def timer_decorator(func: Callable) -> Callable:
def wrapper(*args, **kwargs):
start_time = time()
func(*args, **kwargs)
end_time = time()
print(end_time-start_time)
return wrapper
@timer_decorator
def sleeper(sec: int):
sleep(sec)
sleeper(5)
# 5.00
And, it works as expected!
Returning values from decorated functions
Give it a thought, what would happen if sleeper were to return the number of seconds it was sleeping. Who would have the return value?
According to the current implementation, the decorator is not giving back anything. We just call the func
but are not returning anything from the wrapper function. The returns also need to cascade upwards so that the caller get back the expected value.
from time import time, sleep
from typing import Callable
def timer_decorator(func: Callable) -> Callable:
def wrapper(*args, **kwargs):
start_time = time()
ret_val = func(*args, **kwargs)
end_time = time()
print(end_time-start_time)
return ret_val
return wrapper
@timer_decorator
def sleeper(sec: int):
sleep(sec)
return sec
sleep_time = sleeper(5)
print(sleep_time)
# 5.00 (This print is from the decorator)
# 5 (This is from the print statement)
Passing arguments to decorators
What if we want to have a timer_decorator
that takes in an argument, say in_min
which can be true or false. And if true, then the time should be printed in minutes instead of seconds.
Remember that the only requirement from a decorator is that it should return a function that takes in a function as an argument.
from time import time, sleep
from typing import Callable
def timer_decorator(in_min: bool = False):
def decorator(func: Callable) -> Callable:
def wrapper(*args, **kwargs):
start_time = time()
ret_val = func(*args, **kwargs)
end_time = time()
time_consumed = end_time - start_time
print(time_consumed if in_min is False else time_consumed / 60)
return ret_val
return wrapper
return decorator
@timer_decorator(in_min=True)
def sleeper(sec: int):
sleep(sec)
return sec
sleep_time = sleeper(5)
# 0.08336979548136393 (time consumed in minutes)
In this implementation, timer_decorator
takes in an argument and gives back a decorator (!). This decorator already has access to the variable in_min
and will use it. This might be the most complicated example of the post. Take your time and make sure you understand everything clearly.
Practical use cases of decorators
- Flask uses decorators for routing, authentication guards etc..: It makes the code much more clean and readable
- Decorators can be used for profiling: Like the example we just wrote)
- Retry decorator on certian exceptions: An
@retry
decorator which takes in a list of exception types as arguments and retry's the function if the function exceptions out (Celery does this)
And many more...
Congratulations on coming this far! Decorators are quite tricky in the beggining, but once you understand them. They are really powerful and useful.