Decorators in Python
Decorators in Python

Decorators in Python

What do you do to add functionality to an existing piece of code in Python? The answer is simple. We use Python Decorators. In Python, we use decorators to add functionality to an object. Also, this does not alter the structure of existing code. In this post, we will learn more about decorators in Python.

Before diving into this topic, one should know that everything in Python is an object. Also, various names can be bound to the same function object. Here is an example:

def my_fun(val):
    print(val)

my_fun("Avid Python")

new_fun = my_fun
new_fun("Avid Python")

Output:

Avid Python
Avid Python

Here, the functions my_fun and new_fun both give the same output. They refer to the same function object. We will discuss more about functions in the below sections.

Let us look at a few concepts to better understand decorators.

Assigning Functions to Variables in Python

In Python, we can assign a function to a variable. Then, this variable aids in calling the function. Let us see an example.

def mul(x):
    return x*2

multiply = mul
multiply(4)

Output:

8

Here, we are defining a function ‘mul‘. This multiplies a number with 2. Next, we assign this to another variable ‘multiply‘. Moreover, to call the function ‘mul‘, we use the variable ‘multiply‘.

Defining Functions Inside other Functions in Python

In Python, we can define a function inside another function in Python. This is how it is done:

def mul(x):
    def add(x):
        return x + 1

    output = add(x)
    return output
mul(5)

Output:

6

In the above example, we define the function ‘add‘ inside the function ‘mul‘. The function ‘add‘ is then assigned to the variable ‘output‘.

Passing Functions as Arguments to other Functions in Python

When we call a function, we pass variables as parameters. But we can also pass functions as parameters. Let us look at the illustration below.

def mul(x):
    return x*2

def call_the_function(function):
    value_of_x = 10
    return function(value_of_x)

call_the_function(mul)

Output:

20

In the above code, we are passing a function as an argument.

Functions can return other Functions in Python

This concept allows one function to generate another function. The below example illustrates this.

def function_one():
    def function_two():
        return "Avid Python"
    return "Avid Python"

name = function_one()
name()

Output:

Avid Python

In the above example, the function ‘function_one’ is returning the other function ‘function_two’.

Scope while using Nested Functions

While nesting, the nested function can access the outer scope of the enclosing function. This pattern is called a Closure. Also, closure forms a critical concept in decorators. This is shown below.

def function_one(msg):
    "Avid Python - Inner function"
    def function_two():
        "Avid Python - Nested function"
         print(msg)

    function_two()

function_one("Avid Python is the best.")

Output:

Avid Python is the best.

Suggested Reading: When to use try-except instead of if else in Python?

Back to Decorators in Python

With the above prerequisites in mind, we are all set to learn more about decorators. In the following blocks, we cover the basic concepts of decorators in Python.

Creating Decorators in Python

Let us see how we create Decorators in Python. The one in this example here will convert a sentence to uppercase. In order to do this, we define a wrapper inside the enclosed function.

def upper_dec(fun):
    def wrapper():
        x = fun()
        change_to_uppercase = x.upper()
        return change_to_uppercase
  
    return wrapper

The decorator function takes a function as an argument. And therefore, we define a function and pass it to our decorator. Also, we assign a function to a variable so that we can call the decorator function.

def greet():
    return "welcome to avid python"

decorate = upper_dec(greet)
decorate()

Output:

WELCOME TO AVID PYTHON

Our purpose has been served but guess what? The code is a bit lengthy. That is where the functionality of Python comes in.

A simpler way to change a sentence to uppercase is by using a decorator. We use the @ symbol before the function that we have to decorate. Let us look at the below example to understand this better.

@uppercase_decorator
def greet():
    return "welcome to avid python"

greet()

Output:

WELCOME TO AVID PYTHON

Multiple Decorators to a Single Function in Python

We can apply more than one decorator to a single function. However, one should take care of the order in which the decorators are called. Let us understand this with an example where we are going to use two decorators. We will split a sentence into a list and also change it to uppercase.

def split_function(string):
    def wrapper():
        fun = string()
        after_splitting = fun.split()
        return after_splitting

   return wrapper
@split_string
@uppercase_decorator
def greet():
    return "welcome to avid python"
greet()

Output:

['WELCOME', 'TO', 'AVID', 'PYTHON']

Notice that the decorators are working in the bottom up direction. If we interchange the order of decorators, we will get an error. This happens because lists don’t have an upper attribute. Here, the sentence first gets changed to uppercase and then splits into a list.

Accepting Arguments in Decorator Functions in Python

We can also pass an argument in a decorator. To achieve this, we pass arguments to the wrapper function. These arguments are then further passed to the function that needs decorating.

def arguments_in_decorator(fun):
    def wrapper_arguments(x, y):
        print("Arguments passed are: {0}, {1}".format(x,y))
        fun(x,y)
    return wrapper_arguments

@decorator_with_arguments
def name(first_name, last_name):
    print("The best place to learn Python is {0} {1}".format(first_name, last_name))

name("Avid", "Python")

Output:

Arguments passed are: Avid Python
The best place to learn Python is Avid Python.

Defining General Purpose Decorators in Python

A general purpose decorator is a decorator that can be applied to any function. To do this, we use args, **kwargs.args and **kwargs. The keywords args and kwargs allow us to pass as many arguments as we want.

def decorator_passing_arbitrary_arguments(function_to_decorate):
    def wrapper_accepting_arbitrary_arguments(*args,**kwargs):
        print('Positional arguments:', args)
        print('Keyword arguments:', kwargs)
        function_to_decorate(*args)
    return wrapper_accepting_arbitrary_arguments

@decorator_passing_arbitrary_arguments
def function_with_no_argument():
    print("Zero Arguments here.")

function_with_no_argument()

Output:

Positional arguments: ()
Keyword arguments: {}
Zero Arguments here.

Let us now look at how we use the decorator using positional arguments.

@decorator_passing_arbitrary_arguments
def function_with_arguments(a, b, c):
    print(a, b, c)

function_with_arguments(10, 20, 30)

Output:

Positional arguments: (1, 2, 3)
Keyword arguments: {}
1 2 3

We use keywords for passing keyword arguments. Let us see the below example:

@decorator_passing_arbitrary_arguments
def function_with_keyword_arguments():
    print("This is the use of keyword arguments.")

function_with_keyword_arguments(first_name="Avid", last_name="Python")

Output:

Positional arguments: ()
Keyword arguments: {'first_name': 'Avid', 'last_name': 'Python'}
This is the use of keyword arguments.

Passing Arguments to the Decorator in Python

In python, we can also pass arguments to the decorator itself. To do this, we define a decorator maker. This decorator maker accepts arguments if we define a decorator inside it. Then we define a wrapper function inside this decorator.

def decorator_maker_with_arguments(d_arg1, d_arg2, d_arg3):
    def decorator(func):
        def wrapper(f_arg1, f_arg2, f_arg3) :
            "Wrapper function"
            print("The wrapper can access all the variables\n"
                  "\t- from the decorator maker: {0} {1} {2}\n"
                  "\t- from the function call: {3} {4} {5}\n"
                  "and pass them to the decorated function"
                  .format(d_arg1, d_arg2,d_arg3,
                          f_arg1, f_arg2,f_arg3))
            return func(f_arg1, f_arg2,f_arg3)

        return wrapper

    return decorator

pandas = "Pandas"
@decorator_maker_with_arguments(pandas, "Python","Avid")
def decorated_function_with_arguments(f_arg1, f_arg2,f_arg3):
    print("This is the decorated function and it only knows about its arguments: {0}"
           " {1}" " {2}".format(f_arg1, f_arg2,f_arg3))

decorated_function_with_arguments(pandas, "Avid", "Python")

Output:

The wrapper can access all the variables
    - from the decorator maker: Pandas Python Avid
    - from the function call: Pandas Avid Python
and pass them to the decorated function
This is the decorated function, and it only knows about its arguments: Pandas Avid Python

Debugging Decorators in Python

Decorators are nothing but wrappers of functions. They simply wrap around a function to decorate it. However, this does not let us reach the metadata of the decorated function. Therefore, it poses a problem in debugging.

You can access the name of a python object with the __name__ attribute. But we will not get the correct output in the case of decorators as shown below.

decorated_function_with_arguments.__name__
'wrapper'
decorated_function_with_arguments.__doc__
'This is the wrapper function'

In the above codes, you can see that the __name__ and the __doc__ attributes are printing the attributes of the wrapper function instead of the decorator.

To resolve this, Python provides a functools.wraps decorator. This helps in accessing the attributes of the decorator. Below is the implementation.

import functools

def uppercase_dec(func):
    @functools.wraps(func)
    def wrapper():
        return func().upper()
    return wrapper
@uppercase_dec
def greet():
    "This will welcome you"
    return 'welcome to avid python'

greet()
WELCOME TO AVID PYTHON

Now, check the greet() metadata. It is now pointing to the function’s metadata instead of wrapper’s metadata.

greet.__name__
'greet'
greet.__doc__
'This will welcome you'

In the above examples, you can observe that we are able to access the attributes of the decorator in Python. In order to debug the code easily, you can use functools.wraps while defining decorators.

To Sum it Up

Decorators are one of the most dynamic features of Python. Moreover, it alters the use of subclasses without altering the structure of code. Also, this also keeps the code minimal and neat.

I hope you enjoyed reading this article. To know more about python programming, you may read this article on list comprehension in Python. You might also like this article on Dictionary comprehension in Python.

Stay tuned for more informative articles. Happy Learning!

Donate to Avid Python

Dear reader,
If you found this article helpful and informative, I would greatly appreciate your support in keeping this blog running by making a donation. Your contributions help us continue creating valuable content for you and others who come across my blog. 
No matter how big or small, every donation is a way of saying "thank you" and shows that you value the time and effort we put into writing these articles. If you feel that our article has provided value to you, We would be grateful for any amount you choose to donate. 
Thank you for your support! 
Best regards, 
Aditya
Founder

If you want to Pay Using UPI, you can also scan the following QR code.

Payment QR Code
Payment QR Code

Leave a Reply