Before we learn about decorators, we need to understand a few important concepts related to Python functions. Also, remember, everything in Python is an object, even functions are objects.
Nested Function
We can include one function inside another, known as a nested function. For example,
# function to find the sum of square values
def sum(x, y):
# inner function to find the square of a value
def find_square(num):
return num**2
# call the inner function
sum = find_square(x) + find_square(y)
return sum
# call the outer function
result = sum(5, 4)
print(result)
# Output: 41
In the above example, we have created the find_square()
function inside the sum()
function.
Pass Function as Argument
In Python, we can pass a function as an argument to another function. For example,
# function to find the square of a number
def find_square(num):
return num**2
# function to add square values of two numbers
def sum(func, x, y):
sum = func(x) + func(y)
return sum
# pass find_square() as argument to sum()
result = sum(find_square, 5, 4)
print(result) # 41
In the above example, the sum()
function takes a function as its argument. While calling sum()
, we are passing the find_square()
function as the argument.
In the sum()
function, parameters: func
, x
, and y
become find_square
, 5, and 4, respectively. Hence,
func(x)
becomesfind_square(5)
, that returns 25func(y)
becomesfind_square(4)
, that returns 16
Return a Function as a Value
Similarly, we can also return a function as a return value. For example,
# function to find the sum of numbers
def sum(x, y):
sum = x + y
# function to print the value of sum
def printer():
print('Sum is', sum)
# return the printer() function
return printer
# call the outer function
result = sum(5, 4)
# call the returned function
result()
# Output: Sum is 9
In the above example, the return printer
statement returns the inner printer()
function. This function is now assigned to the result
variable.
That's why, when we call the result()
as the function, we get the output.
Here, when we call the printer()
function (using result()
), it prints the value of sum, which was actually the variable of the sum()
function. However, the execution of sum()
was completed earlier, so the sum should have been destroyed.
The reason we are able to do this is due to the closure function. A closure is simply an inner function that remembers the values and variables in its enclosing scope even if the outer function is done executing.
Python Decorators
A Python decorator is a function that takes in a function and returns it by adding some functionality. Let's see an example.
We will create a decorator function that prints out some information before and after executing another function.
def display_info(func):
def inner():
print('Executing',func.__name__,'function')
func()
print('Finished execution')
return inner
def printer():
print('Hello, World!')
printer()
# Output: Hello, World!
Here, we have created two functions:
printer()
that prints'Hello, World!'
display_info()
that takes a function as its argument has a nested function namedinner()
, and returns the inner function.
We are calling the printer()
function normally, so we get the output 'Hello, World!'
. Now, let's call it using the decorator function.
def display_info(func):
def inner():
print('Executing',func.__name__,'function')
func()
print('Finished execution')
return inner
def printer():
print('Hello, World!')
decorated_func = display_info(printer)
decorated_func()
Output
Executing printer function Hello, World! Finished execution
Working of the above example:
decorated_func = display_info(printer)
- We are now passing the
printer()
function as the argument to thedisplay_info()
. - The
display_info()
function returns the inner function, and it is now assigned to thedecorated_func
variable.
decorated_func()
Here, we are actually calling the inner()
function, where we are printing
'Executing printer function'
(__name__
returns the name of the function)'Hello, World!'
by calling theprinter()
function (usingfunc()
)'Finished execution'
As you can see the decorator function takes the printer()
function as its argument, adds some text before and after its execution and returns it.
@ Symbol with Decorator
Instead of assigning the function call to a variable, Python provides a much more elegant way to achieve this functionality using the @
symbol. For example,
def display_info(func):
def inner():
print('Executing',func.__name__,'function')
func()
print('Finished execution')
return inner
@display_info
def printer():
print('Hello, World!')
printer()
Output
Executing printer function Hello, World! Finished execution
Python Decorator Function with Parameters
Let's see an example of passing parameters to a decorator function. Suppose we have to perform a simple division
def divide(x, b):
return a / b
Here, this function runs fine as long as the value of b
is not 0, but if we pass the value for b
as 0, we will get an exception.
Let's create a decorator function that will prevent this from happening.
def smart_divide(func):
def inner(a, b):
print('Dividing', a, 'by', b)
# prevents the division if value of b is 0
if b == 0:
print('Cannot divide by 0')
return
return func(a, b)
return inner
@smart_divide
def divide(a, b):
return a / b
result1 = divide(32, 4)
print(result1)
result2 = divide(21, 0)
print(result2)
Output
Dividing 32 by 4 8.0 Dividing 21 by 0 Cannot divide by 0 None
As you can see, if
b
is not 0, normal division is performedb
is 0, we get message,'Cannot divide by 0'
Multiple Decorators in Python
We can also use multiple decorators to decorate a function multiple times. Let's see an example.
def decorate_star(func):
def inner(arg):
print('*' * 36)
func(arg)
print('*' * 36)
return inner
def decorate_dollar(func):
def inner(arg):
print('$' * 36)
func(arg)
print('$' * 36)
return inner
@decorate_star
@decorate_dollar
def printer(msg):
print(msg)
printer('Decorating Functions with "#" and "*"')
Output
************************************ $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Decorating Functions with "#" and "*" $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ************************************
In the above example, we have used two decorator functions named decorate_star
and decorate_dollar
to print a series of star and dollar symbols before and after executing the function.
Here, these decorator functions wrap the original function, and they are chained together, known as chaining of decorators.
Note: We can also use a single decorator function multiple times to perform multiple decorations on a function. For example,
def decorate_star(func):
def inner(arg):
print('*' * 36)
func(arg)
print('*' * 36)
return inner
@decorate_star
@decorate_star
def printer(msg):
print(msg)
printer('Decorating Functions with "*"')
Why use Decorators?
Decorators are powerful features that allow us to change the behavior of a function. Here're some of the scenarios why we should decorate.
- Sometimes, we might need to change the working of a function after defining the function. In this case, we can use a decorator to make the function behave differently without actually changing the source code.
- Decorators are used more while performing debugging to test the function's behavior in multiple scenarios.
- We can also add logging and caching using decorators.