Decorators in Python with Examples
Introduction
In python, decorators are used to add some behavior to existing callable (functions, methods, and classes). Additional behavior is achieved by wrapping existing callable inside another function which adds some functionality to it. These wrappers are known as decorators in Python.
To understand decorators, we consider following use case which is often required while programming:
Use Case Example: Measuring Execution Time
Suppose there are different function defined in your program in order to accomplish some task. And you're asked to measure execution time - how long they execute - for each function when they are called.
By now, you must be thinking that - okay, no problem, will go to every functions and timeit
them, right? That's all right when you have to do it for few functions. But if there are lot of functions then you are repeating that timeit
thing for every function, right?
So, what do you do here? You write decorator function for measuring execution time and decorate every function using @decorator_name
which we are going to explain next.
Example
Suppose we have following function in our program:
def function_1():
for i in range(100000):
pass
def function_2():
for i in range(10000000):
pass
def function_3():
for i in range(10000):
pass
def function_4():
for i in range(100000000):
pass
And we want to measure execution time of each function when they are called. To measure execution time we create decorator function called timer
like this:
# Timer decorator
# Here timer is name of decorator
def timer(fn):
from time import perf_counter
def inner(*args, **kwargs):
start_time = perf_counter()
to_execute = fn(*args, **kwargs)
end_time = perf_counter()
execution_time = end_time - start_time
print('{0} took {1:.8f}s to execute'.format(fn.__name__, execution_time))
return to_execute
return inner
Now we use @decorator_name
to decorate any function for which we need to measure execution time like this:
# Timer decorator
def timer(fn):
from time import perf_counter
def inner(*args, **kwargs):
start_time = perf_counter()
to_execute = fn(*args, **kwargs)
end_time = perf_counter()
execution_time = end_time - start_time
print('{0} took {1:.8f}s to execute'.format(fn.__name__, execution_time))
return to_execute
return inner
# Decorator in action
@timer
def function_1():
for i in range(100000):
pass
@timer
def function_2():
for i in range(10000000):
pass
@timer
def function_3():
for i in range(10000):
pass
@timer
def function_4():
for i in range(100000000):
pass
# Making function call
function_1()
function_2()
function_3()
function_4()
Here is the output of the above program:
function_1 took 0.00620240s to execute function_2 took 0.79993060s to execute function_3 took 0.00093230s to execute function_4 took 8.81120080s to execute
Explanation
When we decorate function like this:
@timer
def function_1():
for i in range(100000):
pass
Then this is equivalent to writing:
def function_1():
for i in range(100000):
pass
function_1 = timer(function_1)
Here, function_1
is passed to decorator function timer()
as an argument and whatever it returns is stored in same name i.e. function_1
on the left hand side of statement function_1 = timer(function_1)
. So, remember function_1
on the right hand side of statement is not same as function_1
on the left hand side.
Now lets look at what function timer()
actually returns.
When we call timer()
we have passed function_1
which is received in variable fn in the scope of timer()
function, which is actually free variable in closure or in inner()
function.
And finally timer()
function returns inner()
function which is stored in variable function_1 on the left hand side of statement function_1 = timer(function_1)
. Thus obtained function_1
is said to be deorated by function timer()
.
So, what happens when we call decorated function_1
?
When function_1()
is called then it executes inner()
function. This function adds extra functionality of measuring execution time and it finally returns fn stored in variable to_execute. So what is fn
here? It is the original function function_1
, right? Thus at the end actual function function_1()
is executed doing its normal task.
And the process is same for other function as well.
Key Points:
- Decorator takes a function as an argument.
- Decorator retuns a Closure.
- Closure generally accepts any number of arguments through
(*args, **kwargs)
. - Decorators adds some additional functionality in Closure (inner function).
- Closure function calls the original function.