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()
:
factory()
: This is decorator factory which accepts extra parameter for decoratortimer()
and returnstimer()
function.timer()
: This is actual decorator, it accepts function to be decorated in variable fn and returns closure,inner()
in this case.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
.