Decorators in Python

Decorators in Python allow the programmers to modify the behavior of the function without actually changing the function. Before jumping to the decorators we must understand these things first:

  • how the nested functions work and
  • how to pass a function as an argument to another function.

Nested Function in Python

A nested function in python is a function present inside another function.

def outer_function():
  def inner_function():
    print("Inside the inner function")
  return inner_function

inner_function=outer_function()
print(type(inner_function))
inner_function()

We have a function called outer_function which doesn’t accept any parameters. Inside the outer_function we have defined another function called inner_function, which has a simple print statement. When called, the outer_function returns the inner_function.

We are saving the returned value in a variable and the type of the returned value is a function. To execute the inner_function, we are calling it.

Pass function as an argument in Python

We can pass a function as an argument to other functions. Let’s modify the above program itself.

def inner_function():
  print("Inside the inner function")

def outer_function(func):
    func()

outer_function(inner_function)

We are passing inner_function to  outer_function as an argument and calling the inner_function inside the outer_function.

Decorators in Python

We will now see how we can create decorators in python. Since we have covered the prerequisites, let’s go and create a simple decorator.

def function_to_be_called():
  print("This is the main function")

def decorator_function(func):
  def inner():
    print("Before execution of the function")
    func()
    print("After execution of the function")
  return inner

function_to_be_called=decorator_function(function_to_be_called)  
function_to_be_called()

Our decorator_function accepts a function as an argument so we are creating a new function called function_to_be-called and passed to the decorator_function.

In Python, we can apply a decorator to a function by placing @ symbol before the function we’d like to decorate. The above code can be modified as

def decorator_function(func):
  def inner():
    print("Before execution of the function")
    func()
    print("After execution of the function")
  return inner

@decorator_function
def function_to_be_called():
  print("This is the main function")
  
function_to_be_called()

In the above example, note that the decorators in python extend the functionality of the function “function_to_be_called” without changing its source code.

What is functools.wraps()

Let’s see an example to understand the need for functools.wraps().

def decorator_function(func):
  def inner():
    '''This function extends the function be called'''
    print("Before execution of the function")
    func()
    print("After execution of the function")
  return inner

@decorator_function
def first_function():
  print("This is the fisrt function")

@decorator_function
def second_function():
  print("This is the second function")
  
print(first_function.__name__)
print(first_function.__doc__)
print(second_function.__name__)
print(second_function.__doc__)

When you are printing the name and the docstring of the first_function and the second_function, it prints the name and docstring of the inner function instead of showing the name and the docstring of the first_function and second_function. This will be more confusing if more functions are using the inner function.

One workaround will be manually assigning the name and docstring like below:

def decorator_function(func):
  def inner():
    '''This function extends the function be called'''
    print("Before execution of the function")
    func()
    print("After execution of the function")
  #manually assigning name and docstrings
  inner.__name__=func.__name__  
  inner.__doc__=func.__doc__  
  return inner

@decorator_function
def first_function():
  '''Docstring for 1st function'''
  print("This is the fisrt function")

@decorator_function
def second_function():
  '''Docstring for 2nd function'''
  print("This is the second function")
  
print(first_function.__name__)
print(first_function.__doc__)
print(second_function.__name__)
print(second_function.__doc__)

Now it gets printed properly.

If we are using more decorators in python, we need to write these lines for each of them. Hence the most effective way is to use functools.wraps as a decorator to the inner function, to save time as well as to increase readability.

from functools import wraps

def decorator_function(func):
  @wraps(func)
  def inner():
    '''This function extends the function be called'''
    print("Before execution of the function")
    func()
    print("After execution of the function")
  return inner

@decorator_function
def first_function():
  '''Docstring for 1st function'''
  print("This is the fisrt function")

@decorator_function
def second_function():
  '''Docstring for 2nd function'''
  print("This is the second function")
  
print(first_function.__name__)
print(first_function.__doc__)
print(second_function.__name__)
print(second_function.__doc__)

Multiple Decorators in Python

We can apply multiple decorators to a function. The order of the application of decorators will be bottom-up.

from functools import wraps
def uppercase_convertor(func):
  @wraps(func)
  def wrapper():
    var=func()
    return var.upper()
  return wrapper

def split_string(func):
  @wraps(func)
  def wrapper():
    var=func()
    return var.split()
  return wrapper

@split_string
@uppercase_convertor
def sample_function():
  return 'Hello world'

output=sample_function()
print(output)

The function sample_function has two decorators. First, uppercase_convertor gets called and the uppercase_function will be passed as an argument to the split_string function.

We can also pass arguments to the decorators in python just like we pass arguments to the normal function.

Conclusion

We have read about Decorators in Python and Multiple Decorators in Python. We have also covered Nested Functions in Python and how to Pass a Function as an argument to another function in Python.

Reference

Translate »