Python Decorators


Video Lecture


Decorators provide a simple syntax for calling higher-order functions.

By the definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.


Prerequisites to learn Decorators :

To understand and implement decorators, you must know the following concepts about the functions in details

  1. Functions are first class objects. (higher order function)
  2. In Python, everything is object (functions too) and can be referenced using a variable name.
  3. The concepts of inner functions.
  4. Decorators in python is sort of an extension to the concept closures in python. So, you must know the concepts of closures.

A quick recap with the code example to understand the above said concepts.

Function referenced using a variable name.

def myfun(var):
    print(var)

myfun("Hello")
ref = myfun  ' ref ' is reference of function
ref("Hello") # calling the function using reference
       
OUTPUT
Hello
Hello

Functions can be passed as arguments to another function.

Functions that take other functions as arguments are called higher order functions.

def square(num):
    return(num * num)

def cube(num):
    return(num*num*num)


def calculate(fun, n):
    res = fun(n)
    print(res)

calculate(square, 5) #passing square as argument
calculate(cube, 5) #passing cube as argument
            
OUTPUT
25
125

Furthermore, a function can return another function.

def outer_func():
    def inner_func():
        print("Hello")
    return inner_func

fun = outer_func()
fun()
          
OUTPUT
Hello

Here, inner_func() is a nested function which is defined and returned each time when we call outer_func().

Another concept which is very important that we must know closures in Python.


Decorators in Python

Python decorators are convenient ways to make changes to the functionality of code without making changes to the code.

Decorator allows programmers to modify the behavior of function or class.

Decorators allow us to wrap another function in order to extend the behavior of wrapped function, without permanently modifying it.

In simple words, a decorator takes in a function, adds some functionality and returns it.

# a decorator function
def myDecorator(func):
    # inner function like in closures
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper


def myfunc():
    print('Hello')

# decorating the myfunc function
decorated_myfunc = myDecorator(myfunc)

# calling the decorated version
decorated_myfunc()
                
OUTPUT
Something is happening before the function is called.
Hello
Something is happening after the function is called.

Let's understand what is going in above code.

Decoration happens in the following line of code:

decorated_myfunc = myDecorator(myfunc)
                

In the above code, the var name decorated_myfunc is pointing to the wrapper() inner function, And we are returning wrapper as a function whenever we call myDecorator(func).

However, wrapper() has a reference to the original myfunc() as func, and calls that function between the two calls to print().

Simply :decorators wrap a function, modifying its behavior.

Python has a simple syntax to decorate a function. We can use the @ symbol along with the name of the decorator function and place it above the definition of the function to be decorated. For example,


@myDecorator
def myfunc():
    print('Hello')
 

is equivalent to


def myfunc():
    print('Hello')

# decorating the myfunc function
decorated_myfunc = myDecorator(myfunc)
                

This is just a syntactic sugar to implement decorators.

Code to implement decorator using @ sysmbol.

# a decorator function
def myDecorator(func):
    # inner function like in closures
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@myDecorator
def myfunc():
    print('Hello')

myfunc()
                
OUTPUT
Something is happening before the function is called.
Hello
Something is happening after the function is called.

Decorating Functions With Arguments

Suppose that we have a function that accepts some arguments. Can we still decorate it by the same decorator? Let's try

def myDecorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@myDecorator
def myfunc(name):
    print(f'Hello {name}')

myfunc("World")
                
OUTPUT
Traceback (most recent call last):
TypeError: wrapper() takes 0 positional arguments but 1 was given
                

Unfortunately, above code raises an error:

The The problem is that the inner function wrapper() does not take any arguments, but name="World" was passed to it. We could fix this by letting wrapper() accept one argument.

def myDecorator(func):
    def wrapper(arg): 
        print("Something is happening before the function is called.")
        func(arg) 
        print("Something is happening after the function is called.")
    return wrapper

@myDecorator
def myfunc(name):
    print(f'Hello {name}')

myfunc("World")
                

From the above examples, we can observe that the parameters of the inner wrapper() function inside the decorator is the same as the parameters of functions it decorates. On the basis of above observation, we can make general decorators that work with arbitrary number of parameters.

The solution is to use *args and **kwargs in the inner wrapper function. Then it will accept an arbitrary number of positional and keyword arguments.

CODE:

def myDecorator(func):
    def wrapper(*args, **kwargs):
        print("I am decorating all functions")
        func(*args, **kwargs)
    return wrapper

@myDecorator
def myfunc():
    print("Function without argument")

@myDecorator
def myfunc1(name):
    print(f'function with argument')


myfunc()
print("###########")
myfunc1("Hello")
                
OUTPUT
I am decorating all functions
Function without argument
###########
I am decorating all functions
function with argument

As we can see that both the function myfunc() and myfunc(name) are decorated using the same decorator.


Chaining The Decorators in Python

A function can be decorated multiple times with different or same decorators. We can use more than one decorator to decorate a function by chaining them.

EXAMPLE :

def myDecorator1(func):
    def wrapper(*args, **kwargs):
        print("I am first decorator")
        func(*args, **kwargs)
        print("First Decorator ends")
    return wrapper

def myDecorator2(func):
    def wrapper(*args, **kwargs):
        print("I am second decorator")
        func(*args, **kwargs)
        print("Second Decorator Ends")
    return wrapper

@myDecorator1
@myDecorator2
def myfunc():
    print("Actual statements from function")

myfunc()
                
OUTPUT
I am first decorator
I am second decorator
Actual statements from function
Second Decorator Ends
First Decorator ends
                

The above syntax of,

@myDecorator1
@myDecorator2
def myfunc():
    print("Actual statements from function")

is equivalent to

def myfunc():
    print("Actual statements from function")
fun = myDecorator1(myDecorator2(myfunc))

The order in which we chain decorators matter. If we change the order then output will be change.


Real World Example

Sometimes we want to find the execution time of a function. One way is to calculate the execution time is, write the logic of time calculation within the function and second way is to use the decorator. The question is which one is best suited? The answer is decorators. Why decorator? Answer is, we all know that the function's functionality is to perform the desired task for which that is created, and we want to add extra functionality to calculate the execution time, and this can be done by decorators without modifying the actual code of function. And one more thing, this decorator can be use with any functions.

import time
def time_cal(fun):
    def wrapper(*args,**kwargs):
        start_time = time.time()
        result = fun(*args,**kwargs)
        end_time = time.time()
        print (fun.__name__," execution time taken =",(end_time-start_time)*1000," milli sec")
        return result
    return wrapper

@time_cal
def square(num):
    result = []
    for x in num:
        result.append(x * x)
    return result

@time_cal
def cube(num):
    result = []
    for x in num:
        result.append(x * x * x)
    return result

y = range(1, 100000)
square(y)
cube(y)


                
OUTPUT
square  execution time taken = 15.58685302734375  milli sec
cube  execution time taken = 31.24380111694336  milli sec
                

Next chapter is Anonymous Function






 




Training For College Campus

We offers college campus training for all streams like CS, IT, ECE, Mechanical, Civil etc. on different technologies like
C, C++, Data Structure, Core Java, Advance Java, Struts Framework, Hibernate, Python, Android, Big-Data, Ebedded & Robotics etc.

Please mail your requirement at info@prowessapps.in


Projects For Students

Students can contact us for their projects on different technologies Core Java, Advance Java, Android etc.

Students can mail requirement at info@prowessapps.in


CONTACT DETAILS

info@prowessapps.in
(8AM to 10PM):

+91-8527238801 , +91-9451396824

© 2017, prowessapps.in, All rights reserved