A decorator is a very powerful feature in Python that allows us to decorate or modify a function.
Suppose you are given a task which has to be completed within a day. In the task, you created 50 functions, and after that you realize that you forgot to add the start and end time stamp at the beginning and end respectively of the body of each function. What would you do in that case? In such a scenario, you can use decorators as your rescue.
You can create a decorator which adds a start time stamp at the beginning and an end time stamp at the end of the body of the function it is applied to. After creating this decorator, you can apply it to all the 50 functions to modify them. Hence, in this way, you can modify the functions without actually changing their code. This is the beauty of decorators.
You will understand better what we are talking about when we will dive into decorators, but before that, let’s revise a few concepts which will be used while creating decorators.
Let’s recap
Let’s briefly look at some concepts related to functions which we have already studied.
Assigning a Function to a Variable
We can assign a function to a variable and then can use this variable to call the function.
def multiply(a, b):
return a * b
product = multiply # assigning the function multiply to variable product
print(product(5, 6)) # calling the function using product
We assigned the function multiply()
to the variable product by writing product = multiply
(because function name is the reference to the function). So, product now stores the same function. Thus we have two names product and multiply for the same function and it can be called by any name.
Passing a Function as Argument to another Function
We can pass a function as an argument to another function, just like we pass integers, strings, lists or any other data type as argument.
def display(func):
print("This is the display function")
func()
def message():
print("Welcome everyone!")
display(message)
The function message()
is passed as an argument to the function display()
, making its parameter func equal to the passed function message()
. Inside the display()
function, the function func is called by writing func()
.
Defining a Function inside another Function (Nested Function)
A function which is defined and called inside another function is called a nested function. In the last two chapters, we have seen sufficient examples of nested functions. Let’s again look at an example.
def outer():
def inner(): # defining nested function
print("This is nested function")
print("This is outer function")
inner() # calling nested function
outer() # calling outer function
We defined a function named inner inside another function named outer. Thus, inner()
is a nested function of outer()
. The inner()
function is called inside the outer()
function by writing inner()
.
Returning a Nested Function from its Parent Function
We can return a nested function from the function in which it is defined.
def outer():
def inner(): # defining nested function
print("Welcome everyone!")
return inner # returning nested function
message = outer() # calling outer function
message()
The function inner()
is a nested function of the function outer(). When the outer()
function was called, it returned the inner()
function and assigned it to the variable message.
Look at another example.
def outer(x):
def inner():
print(x)
return inner # returning inner function
message = outer("Hey there!")
message()
This is an example of closure in which the outer()
function returns its nested function inner()
after attaching the value of the variable x
to the code of inner()
. We learned about closures in the previous chapter.
Having revised all the necessary topics, let’s get started with decorators.
What are Decorators?
To understand decorators, let’s first look at the simplest following example of functions.
def normal():
print("I am a normal function")
normal()
Here we have a simple function named normal. Let’s make this function a little bit pretty.
def decorator_func(func):
def inner():
print("****")
func()
print("****")
return inner # returning inner function
def normal():
print("I am a normal function")
decorated_func = decorator_func(normal)
decorated_func()
def decorator_func(func)
→ We created another function named decorator_func
which receives a function as parameter and has a nested function named inner
. As the name suggests, the decorator_func()
function receives a function as parameter, decorates or modifies the inner()
nested function and then returns the decorated inner()
function.
decorated_func = decorator_func(normal)
→ When the normal()
function is passed to the decorator_func()
function, the latter returns the decorated inner()
function and assigns the returned function to the variable decorated_func
. Thus, decorated_func
now stores the decorated function.
decorated_func()
→ Calling the decorated_func()
function now prints the modified output.
In the above example, the decorator_func()
function is a decorator.
Wait, we have just created a decorator! Creating a decorator was this easy.
In the above example, we are assigning the returned decorated function to a new variable decorated_func
by writing decorated_func = decorator_func(normal)
. This doesn’t change our original function normal()
. Assigning the decorated function to normal()
will modify the normal()
function as shown below.
def decorator_func(func):
def inner():
print("****")
func()
print("****")
return inner # returning inner function
def normal():
print("I am a normal function")
normal = decorator_func(normal)
normal()
normal = decorator_func(normal)
→ After taking normal as argument, the decorator_func()
function returns the decorated function to normal only, hence modifying it.
normal()
-> Thus calling the normal()
function now prints the modified output.
So, we can say that we decorated the normal()
function.
There is a much easier way to apply decorator in Python. We can use the symbol @
followed by the name of the decorator function before the definition of the function we want to decorate.
For example, in the above program, we can replace the following piece of code
def normal():
print("I am a normal function")
normal = decorator_func(normal)
with the following code.
@decorator_func
def normal():
print("I am a normal function")
That’s it. Let’s try it out in the previous example.
def decorator_func(func):
def inner():
print("****")
func()
print("****")
return inner # returning inner function
@decorator_func
def normal():
print("I am a normal function")
normal()
We got the same output.
Look at another example in which a decorator converts the text returned by a function to lowercase.
def decorator_lowercase(func):
def to_lower():
text = func()
lowercase_text = text.lower()
return lowercase_text
return to_lower # returning inner function
@decorator_lowercase
def message():
return "I Am a Normal Function"
print(message())
In this example, the function decorator_lowercase()
takes a function as parameter and has a nested function to_lower()
. The nested function to_lower()
converts the string returned by the passed parameter func()
to lowercase and then returns the lowercase string.
Writing @decorator_lowercase
before the definition of the function message()
means that decorator_lowercase()
is applied as the decorator for message()
. This is the same as writing message = decorator_lowercase(message)
.
Passing Parameterized Functions to Decorators
So far, we passed those functions which don’t have parameters to the decorator functions. Now let’s see how to pass a function that has parameters to the decorator function.
If we want to apply a decorator to a function that has parameters, then pass the function as argument to the decorator function and the parameters as arguments to the nested function of the decorator function.
Consider the following function.
def divide(num1, num2):
return num1/num2
The divide()
function has two parameters num1
and num2
. On calling the function, if the argument passed to the parameter num2
is 0, then it will throw an error.
To handle the case of 0 as the second parameter, let’s wrap the function in a decorator as shown below.
def decorator_division(func):
def division(a, b):
if b == 0:
return "Can't divide!"
else:
return a/b
return division # returning inner function
@decorator_division
def divide(num1, num2):
return num1/num2
print(divide(10, 2))
In this program, writing @decorator_division
before the definition of the divide()
function means that decorator_division()
is applied as the decorator for divide()
.
Here, the divide()
function has two parameters, therefore the decorator_division()
function receives the divide()
function as parameter and its nested function division()
takes its two parameters as its parameters. Thus, the parameters a
and b
of the nested function division()
are the same as the parameters num1
and num2
respectively of divide()
.
Inside the nested function, if the second parameter b
is equal to 0, then we are throwing an error message. So, our problem got solved.
Python Chaining Decorators
Till now we have been assigning a single decorator to a function. In Python, we have the flexibility to apply multiple decorators to a single function, and doing this is quite easy.
Look at the following example.
def decorator_star(func):
def inner():
print("****")
func()
print("****")
return inner # returning inner function
def decorator_hash(func):
def inner():
print("####")
func()
print("####")
return inner # returning inner function
@decorator_star
@decorator_hash
def normal():
print("I am a normal function")
normal()
We applied two decorators decorator_star
and decorator_hash
to the normal()
function. Note that the order in which we apply the decorators to the function matters.
In the above example, the following code
@decorator_star
@decorator_hash
def normal():
print("I am a normal function")
is the same as writing the following statements.
@decorator_star
@decorator_hash
def normal():
print("I am a normal function")
You must have understood the order in which decorators are applied to the function. The decorator applied later is applied first. Hence first decorator_hash
is applied and after that decorator_star
is applied.
Let’s see what will be the output if we reverse the order of decorator_star
and decorator_hash
.
def decorator_star(func):
def inner():
print("****")
func()
print("****")
return inner # returning inner function
def decorator_hash(func):
def inner():
print("####")
func()
print("####")
return inner # returning inner function
@decorator_hash
@decorator_star
def normal():
print("I am a normal function")
normal()
And yes we got the output as expected.
So, decorators are used to modify the functionality of a function (or even a class) without actually changing the code of the function.
Congratulations on making it to the end of this tutorial.