Decorator Factory in Python

Source: Deep Learning on Medium

Decorator Factory in Python

Decorators are powerful and useful, but you need to know how to use them properly to make the most out of this wonderful feature. In this article I’ll be discussing how we can use decorators with additional parameters (not the function alone). We will see 2 techniques of doing this, one with a function and one with a class.

Why do we even need this? lets start with a very short intro on decorators and then see one way in which we are limited. *If you already understand decorators feel free to skip to the “after decorators” headline.

Decorators are a python feature that adds some functionality to some function. In practice it means I can write a decorator, which does something and by decorating another function, I add this functionality to a new function, without writing the code twice. An example would make it easy to understand:

def typing(num):
print('typing the number', num)
typing(4)
---> typing the number, 4

This is a rather simple function. Now lets say I have 4 functions and I want to add timing to all of them, I want to know how long did it take to run. One solution would be adding timing functionality to every function, but as you can imagine this isn’t very DRY (Don’t Repeat Yourself). Instead what we do is we wright this timing function once, and add (decorate) this function to any other function that wants “its services”.

# Timing functionality from Python's built-in module
from time import perf_counter
def decorating(fn):
def inner(*args):
start = perf_counter()
result = fn(*args)
end = perf_counter()
elapsed = end - start
print(result)
print('elapsed', elapsed)
return inner

So we have a function that starts calculating time, Runs the original function (the function using the timing services), calculates time again on a new variable and returns the difference (e.g time elapsed). Lets define a function that calculates a factorial, and add to it a timing functionality.

@decorating
def calc_factorial(num):
if num < 0:
raise ValueError('Please use a number not smaller than 0')
product = 1
for i in range(num):
product = product * (i+1)
return product
calc_factorial(4)
--->
24
elapsed 5.719979526475072e-06
calc_factorial(10)
--->
3628800
elapsed 6.7150103859603405e-06

And there it is! we get timing functionality for free, with only adding this @decorating symbol. You can imagine why this is much more scalable than writing everything again and again. Those are decorators, this is the main idea.

After Decorators

We’ve seen how decorators could be very useful, that being said, writing them this way has one important limitation we’ll discuss. What happens if I want to add some parameters to my decorating function that will be later on used by the decorated function?. meaning, what if I not only want to add functionality, but also transfer data. For example say I’m testing different ways to write the same function for optimization purposes. I want to run the same function many times and get an average of the time it took. Can I pass the number of loops as a parameter? not like this. To be able to also transfer data we need to create what is called a Decorator Factory

* Like anything this could be done in many ways, we will use a way that demonstrates transferring the data all the way from the Decorator Factory.

Decorator Factory

A decorator factory is a function that returns a decorator. Instead of returning a function (inner in our case), we will return a decorator (named “dec”). Lets see this:

from time import perf_counterdef decorator_factory(loops_num):
def decorating(fn):
def inner(num):
total_elapsed = 0
for i in range(loops_num):
start = perf_counter()
result = fn(num)
end = perf_counter()
total_elapsed += end - start
avg_run_time = total_elapsed/loops_num
print('result is', result)
print('num of loops is', loops_num)
print('avg time elapsed', avg_run_time)
return inner
return decorating

See that here we return both the inner function and the decorating function. What we are returning, is a decorator. This enables the inner function to have access to some additional parameters (e.g a, b) and use them.
* For this particular example we will be using num and not *args to make it a bit more understandable. Usually *args could be preferable as its more flexible.

Now we don’t only have the timing functionality, but we also have access to more parameters, something we could not have done earlier.

@decorator_factory(500)
def calc_factorial2(num):
if num < 0:
raise ValueError('Please use a number not smaller than 0')
product = 1
for i in range(num):
product = product * (i+1)
return product
calc_factorial2(4)
--->
result is 24
num of loops is 500
avg time elapsed 1.5613697469234467e-06

Now we can use the looping functionality with other functions as well.

@decorator_factory(5)
def string(s):
print(s)
string('this is working'
--->
this is working
this is working
this is working
this is working
this is working
num of loops is 5

Write once, run everywhere.

Class Decorator Factory

Lets now look at an alternative way to generate the same behavior, but with a class. A class is sometimes easier to make more complicated operations with, and so its an important tool to have. Lets do exactly what we did with the decorator factory function, this time with a class.

class Decorator_Factory_Class:
def __init__(self, num_loops):
self.num_loops = num_loops
def __call__(self, fn):
def inner(num):
total_elapsed = 0
for i in range(self.num_loops):
start = perf_counter()
result = fn(num)
end = perf_counter()
total_elapsed += end - start
avg_run_time = total_elapsed/self.num_loops
print('num of loops is', self.num_loops)
return result
return inner

As a result, we can now use the class the same way we used the function

@Decorator_Factory_Class(5)
def calc_factorial2(num):
if num < 0:
raise ValueError('Please use a number not smaller than 0')
product = 1
for i in range(num):
product = product * (i+1)
return product
calc_factorial2(4)
--->
num of loops is 5
avg_run_time is 2.301810309290886e-06
24

So … we’ve seen how decorators function, what are their benefits and what could be a meaningful limitation. We’ve also seen how we can overcome this limitation by defining a decorator factory, which returns a decorator. Defining a few decorators like these, could make the code less repetitive, faster to develop and less bugs prone.

Hope you’ve gained something important !

For any questions/suggestions/constructive hate, feel free to reach out at Bakugan@gmail.com or at Linkedin