Decorator with Parameters (Decorator Factory Concept)

Decorator with parameters also known as decorator factory are wrapper around existing decorators and they return decorator function.

Introduction

Python decorators are used to extend behavior of existing callable (functions, methods, and classes) by wrapping existing callable inside function.

Decorator with parameters are wrapper around existing decorators.

Decorator returns closure but decorator with parameters returns decorator.

Decorator with parameters are also known as decorator factory because they return actual decorator function which extends the behavior of callables.

To understand decorators with parameters, lets consider following scenario which is often required while programming:

Use Case Scenario: Decorator Factory

Suppose there are different function defined in your program in order to accomplish some task. And you're asked to measure average execution time. Average execution time is calculated by executing function which repeats given number of times. Given number can be different for each function.

So, what do you do here?

You write decorator function for average measuring execution time which accepts some number and decorate every function using @decorator_name(number) which we are going to explain next.

Example: Decorator Factory


# name factory is decorator factory which accepts parameters
def factory(number):
		
	# name timer is actual decorator
    def timer(fn):
        from time import perf_counter
				
		# name inner is closure
        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 timer

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

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

function_1()
function_2()

Output

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

Explanation

First, lets discuss the purpose of three functions factory(), decorator() & inner():

  1. factory(): This is decorator factory which accepts extra parameter for decorator timer() and returns timer() function.
  2. timer(): This is actual decorator, it accepts function to be decorated in variable fn and returns closure, inner() in this case.
  3. inner(): This is closure which performs additional task (objective of decorator) and returns original function received in variable fn.

To understand decorators with parameters, let's consider following code from above example. And here we are going to break it down step by step.


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

function_1()

This is equivalent to writing:


def function_1():
    for i in range(1000000):
        pass

factory(50)(function_1)()

Again, this is equivalent to writing:


def function_1():
    for i in range(1000000):
        pass

decorator = factory(50)
closure = decorator(function_1)
closure()

On calling decorator factory i.e. factory(50), it returns decorator timer() in variable decorator.

Again decorator(function_1) is called. function_1 is received in variable fn. decorator() returns inner() function which is assigned to variable closure.

Thus obtained inner() function stored in closure is said to be decorated because on calling it, it first performs task of measuring average execution time and returns original function function_1 stored in variable to_execute.

And finally, on returning to_executes, it executes original function function_1.