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)