I have some code I want to improve. I got a suggestion to use a complex solution using functools
which I could not understand.
Code Explanation: I am trying to create converter for Strings. What I want to do here is to run some fixed code before a convert function is executed. However that execution depends on variable argument like country code and validation lengths for that string.
This is what I implemented taking inspiration from: https://www.scaler.com/topics/python/python-decorators/
I don't understand why we need 3 levels of functions nesting here just to implement decorator that requires arguments country_code and valid_lengths.
import functools
from collections.abc import Callable
class Number:
def prevalidate(country_code: str, valid_lengths: list[int]): # type: ignore
def decorator(func: Callable):
@functools.wraps(func)
def wrapper(num: str, validate=False):
if num.startswith(country_code):
num = num[2:]
if validate and len(num) not in valid_lengths:
raise ValueError(f"{num} is not valid {country_code} number")
return func(num, validate)
return wrapper
return decorator
@staticmethod
@prevalidate(country_code="DZ", valid_lengths=[13])
def convert_dz(num: str, validate=False) -> str:
return num[4:6] + num[-4:]
... # other similar methods
num = Number.convert_dz("W/2011/012346") # => 1012346
Let me explain each level first.
Now, you wonder why level 1. and 2. are not merged. Indeed they can be merged, but the three layers are motivated by the shortcut given by the @
symbol. The @deco
on a function func
is equivalent to overwriting the name of the function with func = deco(func)
, and @deco_factory(args)
is equivalent to deco = deco_factory(args); func=deco(func)
. So it is the @
symbol which will only pass the function as single argument. Still, you can manually decorate functions, but you may confuse other python developers which are already used to the three layer design.
Edit:
I did not yet comment to your code example, but just explained the title question. Note, that every time you call the decorator factory with the same arguments, you are creating a new decorator. It would be better if you just reuse a single instance of the decorator. Moreover, if the input values of the decorator factory change the way your class Number
behaves, you should better add those values to the class constructor, I mean the __init__
method, and work with instances of Number
.
Now, the implementation may not require a decorator, because adding self.prevalidate(num)
at the beginning of each function is just a one-liner and is more explicit than the decorator, but there might be more ways to achieve it.