A decorator is a function that takes another function and extends the behaviour of the latter function without explicitly modifying it. Decorator provides a simple syntax for calling higher order functions. A higher order function is a function which takes one or more functions as its arguments or returns a function (or both). Decorator is an application of higher order functions and nested functions.

Simple Decorators

The code below shows a simple decorator that prints a message before and after execution of decorated function.

def hello_decorator(func):
    def wrapper():
        print('Before calling func')
        func()
        print('After calling func')

    return wrapper


def hello():
    print('Hello, World!')


decorated_hello = hello_decorator(hello)
decorated_hello()

Output:

Before calling func
Hello, World!
After calling func

Most of the parts of this program are self explanatory. We have a hello decorator which accepts a function and returns a wrapper function. The wrapper function first prints a message, then calls the function passed to the decorator, and then prints another message. Our hello function simply prints 'Hello, World!' but we have transformed this function by passing it through hello_decorator.

Instead of manually creating decorated functions, we can use the simple decorating syntax provided by Python. The above program can be simplified as follows.

def hello_decorator(func):
    def wrapper():
        print('Before calling func')
        func()
        print('After calling func')

    return wrapper


@hello_decorator
def hello():
    print('Hello, World!')


hello()

The output of this program will be same as the first program but now our program is much shorter and simpler. We are no longer creating decorated function by passing our function to decorator. Instead we use the @decorator syntax in Python.

The examples above is not very useful since the do not do much other than printing some decorator. A more practical use is explained in the next example. Here we use a decorator to find the execution time for a function.

import time


def timeit(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print("{} took {} seconds to execute".format(func.__name__, end_time - start_time))

    return wrapper


@timeit
def hello():
    time.sleep(2)
    print("Hello, World!")

hello()

The output will be something like this:

Hello, World!
hello took 2.002326726913452 seconds to execute

Note that we have added a 2 seconds wait in hello function to see some real difference between start time and end time.

Decorating Functions Which Accepts Arguments

In the above examples, we decorated simple functions. None of them were accepting any parameters. The example below demonstrates decorating a function which accepts arguments.

def add_ten(func):
    def wrapper(num1, num2):
        num1, num2 = num1+10, num2+10
        return func(num1, num2)

    return wrapper


@add_ten
def adder(num1, num2):
    return num1 + num2


print(adder(1, 2)) # prints 23

The wrapper function needs to accepts all the arguments that are passed to the decorated function (that is adder in our example). We then modified the parameters by adding a value to them (Cheating may be!).

The slightly more complicated decorator which accepts arbitrary number of arguments is given below.

def add_ten(func):
    def wrapper(*args, **kwargs):
        args = map(lambda x: x + 10, args)
        kwargs = dict(map(lambda key: (key, kwargs[key] + 10), kwargs))

        return func(*args, **kwargs)

    return wrapper


@add_ten
def numbers(*args, **kwargs):
    print(args)
    print(kwargs)


numbers(1, 2, 3)
numbers(1, 2, 3, a=4, b=5)

The output will be

(11, 12, 13)
{}
(11, 12, 13)
{'a': 14, 'b': 15}

The next example builds on top of the previous one. Here we use the add_ten decorator to create a corrupted average function which always give a 10 more than the actual average.

from functools import reduce


def add_ten(func):
    def wrapper(*args, **kwargs):
        args = map(lambda x: x + 10, args)
        kwargs = dict(map(lambda key: (key, kwargs[key] + 10), kwargs))

        return func(*args, **kwargs)

    return wrapper


@add_ten
def average(*args, **kwargs):
    all_args = list(args) + list(kwargs.values())
    return reduce(lambda x, y: x + y, all_args) / len(all_args)


print(average(1, 2, 3, a=4, b=5))

Posted on
Category: Python
Tags: Python, Decorators, Functional Programming