Python Decorators

In this tutorial, you will learn about Python decorators with the help of examples.

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) becomes find_square(5), that returns 25
  • func(y) becomes find_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 named inner(), 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 the display_info().
  • The display_info() function returns the inner function, and it is now assigned to the decorated_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 the printer() function (using func())
  • '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 performed
  • b 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.
Did you find this article helpful?