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). A 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 the execution of the 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 the same as the first program but now our program is much shorter and simpler. We are no longer creating a decorated function by passing our function to the decorator. Instead, we use the @decorator syntax in Python.

The examples above are not very useful since they do not do much other than printing some message. 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 the start time and end time.

Decorating Functions Which Accepts Arguments

In the above examples, we decorated simple functions. None of them was 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 accept all the arguments that are passed to the decorated function (that is the adder function in our example). We then modified the parameters by adding value to them (Cheating!).

The slightly more complicated decorator which accepts an 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 gives 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))

Last updated on July 4, 2017
Tags: Python Decorators Functional Programming