Signup/Sign In

Python Decorators

As the name suggests, decorators in python are special functions which adds additional functionality to an existing function or code.

For example, you had a white car with basic wheel setup and a mechanic changes the color of your car to red and fits alloy wheels to it then the mechanic decorated your car, similarly a decorator in python is used to decorate(or add functionality or feature) to your existing code.


Prerequisite

Before we learn the concept of decorators we must know a few things about functions.

In Python everything is an object and can be referenced using a variable name. Yes, even functions are objects with attributes.

We can have multiple variables reference the same function object(definition), for example:

def one(msg):
    print(msg)
# calling the function
one("Hello!")

# having a new variable reference it
two = one

# calling the new variable
two("Hello!")

Hello! Hello!

Similarly, we can pass a function as an argument too. For example:

# some function
def first(msg):
    print(msg)

# second function
def second(func, msg):
    func(msg)

# calling the second function with first as argument
second(first, "Hello!")

Hello!

While in the example above the function second took the function first as an argument and used it, a function can also return a function.

When there is nested functions(function inside a function) and the outer function returns the inner function it is known as Closure in python, about which we learned in our last tutorial.

Decorators in python is sort of an extension to the concept closures in python.


Using Decorators in Python

A decorator gives a function a new behavior without changing the function itself. A decorator is used to add functionality to a function or a class. In other words, python decorators wrap another function and extends the behavior of the wrapped function, without permanently modifying it.

Now, Let's understand the decorators using an example,

# a decorator function
def myDecor(func):
    # inner function like in closures
    def wrapper():
        print("Modified function")
        func()
    return wrapper


def myfunc():
    print('Hello!!')

# Calling myfunc()
myfunc()

# decorating the myfunc function
decorated_myfunc = myDecor(myfunc)

# calling the decorated version
decorated_myfunc()

Modified function Hello!!

In the code example above, we have followed the closure approach but instead of some variable, we are passing a function as argument, hence executing the function with some more code statements.

We passed the function myfunc as argument to the function myDecor to get the decorated version of the myfunc function.

Now rather than passing the function as argument to the decorator function, python provides us with a simple way of doing this, using the @ symbol.

For example,

# using the decorator function
@myDecor
def myfunc():
    print('Hello!!')

# Calling myfunc()
myfunc()

Modified function Hello!!

In the code example above, @myDecor is used to attach the myDecor() decorator to any function you want. So when we will call myfunc(), instead of execution of the actual body of myfunc() function, it will be passed as an argument to myDecor() and the modified version of myfunc() is returned which will be executed.

So, basically @<Decorator_name> is used to attach any decorator with name Decorator_name to any function in python programming language.


Decorators with arguments

Till now we have seen the use of decorators to modify function that hasn't used any argument. Now, let's see how to use argument with a function which is to be decorated.

For this, we are going to use *args and **kwargs as the arguments in the inner function of the decorator.

The *args in function definition is used to pass a variable number of arguments to any function. It is used to pass a non-keyworded, variable-length argument list.

The **kwargs in function definitions is used to pass a keyworded, variable-length argument list. We use the name kwargs with the double star. The reason is that the double star allows us to pass through keyword arguments (and any number of them).

For example,

def myDecor(func):
    def wrapper(*args, **kwargs):
        print('Modified function')
        func(*args, **kwargs)
    return wrapper

@myDecor
def myfunc(msg):
    print(msg)

# calling myfunc()
myfunc('Hey')

Modified function Hey

In the example, the myfunc() function is taking an argument msg which is a message that will be printed. The call will result in the decorating of function by myDecor decorator and argument passed to it will be as a result passed to the args of wrapper() function which will again pass those arguments while calling myfunc() function. And finally, the message passed will be printed after the statement 'Modified function'.


Chaining the Decorators

We can use more than one decorator to decorate a function by chaining them. Let's understand it with an example,

# first decorator
def star(f):
    def wrapped():
        return '**' + f() + '**'
    return wrapped

# second decorator
def plus(f):
    def wrapped():
        return '++' + f() + '++'
    return wrapped

@star
@plus
def hello():
    return 'hello'

print(hello())

**++hello++**

In the above example, star and plus decorators are defined that can add the ** and ++ to our message. These both are attached to hello() function and hence they simultaneously modified the function, decorating the output message.


Practical use of Decorators

Decorators are very often used for adding the timing and logging functionalities to the normal functions in a python program. Let's see one example where we will add the timing functionalities to two functions:

import time

def timing(f):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = f(*args,**kwargs)
        end = time.time()
        print(f.__name__ +" took " + str((end-start)*1000) + " mil sec")
        return result
    return wrapper

@timing
def calcSquare(numbers):
    result = []
    for number in numbers:
        result.append(number*number)
    return result

@timing
def calcCube(numbers):
    result = []
    for number in numbers:
        result.append(number*number*number)
    return result

# main method
if __name__ == '__main__':
    array = range(1,100000)
    sq = calcSquare(array)
    cube = calcCube(array)

calcSquare took 60.42599678039551 mil sec calcCube took 52.678823471069336 mil sec

In the above example, we have created two functions calcCube and calcSquare which are used to calculate square and cube of a list of numbers respectively. Now, we want to calculate the time it takes to execute both the functions, for that we have defined a decorator timing which will calculate the time it took in executing both the functions.

Here we have used the time module and the time before starting a function to start variable and the time after a function ends to end variable. f.__name__ gives the name of the current function that is being decorated. The code range(1,100000) returned a list of numbers from 1 to 100000.

So, by using decorators, we avoided using the same code in both the functions separately (to get the time of execution). This helped us in maintaining a clean code as well as reduced the work overhead.