Decorators in Python


Bhaskar S 04/18/2015


Introduction

Often times there is a need to modify the behavior of one or more functions (or methods) without actually modifying them. For example, we want to monitor some functions (or methods) for their performance by timing them. How do we do that in Python without explicitly modifying those functions (or methods) ??? Enter Decorators in Python.

A Decorator in Python is a function that dynamically alters the behavior of another function without actually modifying it.

Decorators in Python are nothing more than wrapper functions that leverage functional programming constructs (higher-order functions, in particular) to achieve the desired functionality.

In the article Python Quick Notes :: Part - 6, we learnt about the functional programming constructs such as First Class objects and Higher Order functions. Also, from the article Introspection in Python, we learnt that every entity in Python is an object - be it a module, a package, a class, a method, a function, or a type.

The following is a simple python program named DecoratorsOne.py that invokes three functions:

DecoratorsOne.py
#
# Name: DecoratorsOne.py
#

import time

# ----- Method get_customer_info -----

def get_customer_info(cus):
    store = dict(alice=('alice', 'Alice', 'Painter'), bob=('bob', 'Bob', 'Plumber'),
                 carol=('carol', 'Carol', 'Teacher'))
    time.sleep(1)
    return store[cus]

# ----- Method get_deposit_amt -----

def get_deposit_amt(cus):
    store = dict(alice=('alice', 1201.21), bob=('bob', 1217.34),
                 carol=('carol', 1223.47))
    time.sleep(1)
    return store[cus]

# ----- Method get_credit_amt -----

def get_credit_amt(cus):
    store = dict(alice=('alice', 102.73), bob=('bob', 111.85),
                 carol=('carol', 123.91))
    time.sleep(1)
    return store[cus]

# ----- Main -----

if __name__ == '__main__':
    alice = get_customer_info('alice')

    print("DecoratorsOne:: <main> :: alice = " + str(alice))

    bob = get_deposit_amt('bob')

    print("DecoratorsOne:: <main> :: bob = " + str(bob))

    carol = get_credit_amt('carol')

    print("DecoratorsOne:: <main> :: carol = " + str(carol))

Executing DecoratorsOne.py produces the following output:

Output.1

DecoratorsOne:: <main> :: alice = ('alice', 'Alice', 'Painter')
DecoratorsOne:: <main> :: bob = ('bob', 1217.34)
DecoratorsOne:: <main> :: carol = ('carol', 123.91)

We want to instrument the functions get_customer_info, get_deposit_amt, and get_credit_amt to monitor for their performance.

The following is a simple python program named DecoratorsTwo.py that demonstrates how to implement and use Decorators in Python:

DecoratorsTwo.py
#
# Name: DecoratorsTwo.py
#

import time

# ----- Method time_profiler -----

def time_profiler(func):
    def function_wrapper(*args, **kwargs):
        _start = time.time()
        ret = func(*args, **kwargs)
        _end = time.time()
        print("DecoratorsTwo:: <time_decorator> :: {} took {} ms".format(func.__name__, _end-_start))
        return ret
    return function_wrapper


# ----- Method get_customer_info -----

@time_profiler
def get_customer_info(cus):
    store = dict(alice=('alice', 'Alice', 'Painter'), bob=('bob', 'Bob', 'Plumber'),
                 carol=('carol', 'Carol', 'Teacher'))
    time.sleep(1)
    return store[cus]

# ----- Method get_deposit_amt -----

@time_profiler
def get_deposit_amt(cus):
    store = dict(alice=('alice', 1201.21), bob=('bob', 1217.34),
                 carol=('carol', 1223.47))
    time.sleep(1)
    return store[cus]

# ----- Method get_credit_amt -----

@time_profiler
def get_credit_amt(cus):
    store = dict(alice=('alice', 102.73), bob=('bob', 111.85),
                 carol=('carol', 123.91))
    time.sleep(1)
    return store[cus]

# ----- Main -----

if __name__ == '__main__':
    alice = get_customer_info('alice')

    print("DecoratorsTwo:: <main> :: alice = " + str(alice))

    bob = get_deposit_amt('bob')

    print("DecoratorsTwo:: <main> :: bob = " + str(bob))

    carol = get_credit_amt('carol')

    print("DecoratorsTwo:: <main> :: carol = " + str(carol))

Executing DecoratorsTwo.py results in the following output:

Output.2

DecoratorsTwo:: <time_decorator> :: get_customer_info took 1.00109291077 ms
DecoratorsTwo:: <main> :: alice = ('alice', 'Alice', 'Painter')
DecoratorsTwo:: <time_decorator> :: get_deposit_amt took 1.00104689598 ms
DecoratorsTwo:: <main> :: bob = ('bob', 1217.34)
DecoratorsTwo:: <time_decorator> :: get_credit_amt took 1.0010509491 ms
DecoratorsTwo:: <main> :: carol = ('carol', 123.91)

In the Python program DecoratorsTwo.py, the function time_profiler(func) is the Decorator function that takes another function (func) as an argument and returns a modified version of the function (func) that transparently implements the performance measurement functionality we desire.

The inner function function_wrapper(*args, **kwargs) is where the core logic implemented. The arguments of the inner function are:

To use the Decorator function, prepend each function definition with the @ symbol followed by the Decorator function name (time_profiler in our example). Thats it !!!

References

Python Quick Notes :: Part - 1

Python Quick Notes :: Part - 2

Python Quick Notes :: Part - 3

Python Quick Notes :: Part - 4

Python Quick Notes :: Part - 5

Python Quick Notes :: Part - 6

Introspection in Python

Python 'with' Statement

Futures in Python