Decorator Class in Python with Examples

Decorator class are based on the fact that Python class instances can be made callable using dunder __call__.

Before going to decorator class, we first understand what __call__ does to Python class using following simple example:


class A:
    def __call__(self, msg):
        return msg

# Creating instance of class A
instance = A()

# Calling instance - this is possible due to __call__ dunder
instance('Hello There!')

Output of the above program is:

'Hello There!'

In this example argument 'Hello There!' is received in parameter msg which is then returned.

Decorator Class Without Parameters

Now to understand decorator class we take example translation approach. We first consider decorator function and then translate it to decorator class.

Consider following decorator function for logging example with output:


# Logger decorator

def logger(fn):
    from datetime import datetime, timezone
    
    def inner(*args, **kwargs):
        called_at = datetime.now(timezone.utc)
        to_execute = fn(*args, **kwargs)
        print('{0} executed. Logged at {1}'.format(fn.__name__, called_at))
        return to_execute
    
    return inner

@logger
def function_1():
    pass

@logger
def function_2():
    pass

@logger
def function_3():
    pass

@logger
def function_4():
    pass

function_1()
function_4()
function_2()
function_3()
function_1()
function_4()

Output of the above program is:

function_1 executed. Logged at 2020-05-26 03:23:08.714904+00:00
function_4 executed. Logged at 2020-05-26 03:23:08.714904+00:00
function_2 executed. Logged at 2020-05-26 03:23:08.715902+00:00
function_3 executed. Logged at 2020-05-26 03:23:08.715902+00:00
function_1 executed. Logged at 2020-05-26 03:23:08.715902+00:00
function_4 executed. Logged at 2020-05-26 03:23:08.715902+00:00

This decorator function can be written using decorator class as:


class Logger:
    def __call__(self, fn):
        from datetime import datetime, timezone
        
        def inner(*args, **kwargs):
            called_at = datetime.now(timezone.utc)
            to_execute = fn(*args, **kwargs)
            print('{0} executed. Logged at {1}'.format(fn.__name__, called_at))
            return to_execute
        
        return inner

@Logger()
def function_1():
    pass

@Logger()
def function_2():
    pass

@Logger()
def function_3():
    pass

@Logger()
def function_4():
    pass

function_1()
function_4()
function_2()
function_3()
function_1()
function_4()

Output of the above program is:

function_1 executed. Logged at 2020-05-27 06:55:19.480427+00:00
function_4 executed. Logged at 2020-05-27 06:55:19.480427+00:00
function_2 executed. Logged at 2020-05-27 06:55:19.481426+00:00
function_3 executed. Logged at 2020-05-27 06:55:19.481426+00:00
function_1 executed. Logged at 2020-05-27 06:55:19.481426+00:00
function_4 executed. Logged at 2020-05-27 06:55:19.481426+00:00

In decorator class, actual functionality of logger is wrapped between __call__ dunder.

While decorating function using decorator class there is a slight difference. When there are no parameters to decorator; in case of decorator function, functions are decorated using @decorator_name (no parentheses i.e. @logger) but in case of decorator class functions are decorated using @Class_decorator_name() (parentheses i.e. @Logger()).

Decorator Class With Parameters

Next we will consider translating decorator function with parameters to decorator class.

Consider following decorator function with parameters for calculating average execution time for a function with output:


# Decorator class with parameters

# Timer decorator with parameter
def timer(number):

    def decorator(fn):
        from time import perf_counter

        def inner(*args, **kwargs):
            total_time = 0
            for i in range(number):
                start_time = perf_counter()
                to_execute = fn(*args, **kwargs)
                end_time = perf_counter()
                execution_time = end_time - start_time
                total_time += execution_time
            average_time = total_time/number
            print('{0} took {1:.8f}s on an average to execute (tested for {2} times)'.format(fn.__name__, execution_time, number))
            return to_execute

        return inner
    return decorator

@timer(50)
def function_1():
    for i in range(1000000):
        pass

@timer(5)
def function_2():
    for i in range(10000000):
        pass

function_1()
function_2()

Output of the above program is:

function_1 took 0.04114090s on an average to execute (tested for 50 times)
function_2 took 0.46309030s on an average to execute (tested for 5 times)

This decorator function with parameters can be written using decorator class having __init__ dunder to receive parameters and __call__ to extend some functionality. Here is the Python source code:


# Decorator class with parameters

class Timer:
    def __init__(self,number):
        self.number = number
        
    def __call__(self, fn):
        from time import perf_counter
        def inner(*args, **kwargs):
            total_time = 0
            for i in range(self.number):
                start_time = perf_counter()
                to_execute = fn(*args, **kwargs)
                end_time = perf_counter()
                execution_time = end_time - start_time
                total_time += execution_time
            average_time = total_time/self.number
            print('{0} took {1:.8f}s on an average to execute (tested for {2} times)'.format(fn.__name__, execution_time, self.number))
            return to_execute
        return inner

@Timer(50)
def function_1():
    for i in range(1000000):
        pass

@Timer(5)
def function_2():
    for i in range(10000000):
        pass

function_1()
function_2()

Output of the above program is:

function_1 took 0.04112600s on an average to execute (tested for 50 times)
function_2 took 0.69778100s on an average to execute (tested for 5 times)