I'm writing a class that has a number of methods operating on similar types of arguments:
class TheClass():
def func1(self, data, params, interval):
....
def func2(self, data, params):
....
def func3(self, data, interval):
....
def func4(self, params):
....
...
There's a certain convention about these arguments (e.g. data
/params
should be numpy.farrays
, interval
- a list
of 2 floats
), but I'd like to allow user to have more freedom: e.g. functions should accept int
as data
or params
, or only one int
as an interval
and then assume that this is the end point with the starting point 0
, etc.
So to avoid all these transformations within methods, that should do only the logic, I'm using decorators like this:
def convertparameters(*types):
def wrapper(func):
def new_func(self, *args, **kwargs):
# Check if we got enough parameters
if len(types) > len(args):
raise Exception('Not enough parameters')
# Convert parameters
new_args = list(args)
for ind, tip in enumerate(types):
if tip == "data":
new_args[ind] = _convert_data(new_args[ind])
elif tip == "params":
new_args[ind] = _convert_params(new_args[ind])
elif tip == "interval":
new_args[ind] = _convert_interval(new_args[ind])
else:
raise Exception('Unknown type for parameter')
return func(self, *new_args, **kwargs)
return new_func
return wrapper
Where _convert_data
, _convert_params
and _convert_interval
do the dirty job. Then I define the class as follows:
class TheClass():
@convertparameters("data", "params", "interval")
def func1(self, data, params, interval):
....
@convertparameters("data", "params")
def func2(self, data, params):
....
@convertparameters("data", "interval")
def func3(self, data, interval):
....
@convertparameters("params")
def func4(self, params):
....
...
It does the trick, but there're several quite disturbing things:
- It clutters the code (though it is a minor issue, and I find this solution to be much more compact than explicit call of the transforming function at the beginning of each method)
- If I need to combine this decorator with another decorator (
@staticmethod
or something that post-process output of the method) the ordering of these decorators matter - The most disturbing thing is that the decorator of this form completely hides the parameter structure of the method and IPython shows it as
func1(*args, **kwargs)
Are there any better (or more "Pythonic") ways to do such massive parameter transformation?
UPDATE 1: SOLUTION based on n9code's suggestion
In order to avoid confusion there's a modification of the convertparameters
wrapper that solves 3rd issue (masking of signature and docstring of methods) - suggested by n9code for Python >2.5.
Using decorator
module (to be installed separately: pip install decorator
) we can transfer all function's "metadata" (docstring, name and signature) at the same time getting rid of nested structure inside the wrapper
from decorator import decorator
def convertparameters(*types):
@decorator
def wrapper(func, self, *args, **kwargs):
# Check if we got enough parameters
if len(types) > len(args):
raise Exception('Not enough parameters')
# Convert parameters
new_args = list(args)
for ind, tip in enumerate(types):
if tip == "data":
new_args[ind] = _convert_data(new_args[ind])
elif tip == "params":
new_args[ind] = _convert_params(new_args[ind])
elif tip == "interval":
new_args[ind] = _convert_interval(new_args[ind])
else:
raise Exception('Unknown type for parameter')
return func(self, *new_args, **kwargs)
return wrapper
UPDATE 2: MODIFIED SOLUTION based on zmbq's suggestion
Using inspect
module we could also get rid of arguments of decorator, checking the names of arguments of the initial function. This will eliminate another layer of the decorator
from decorator import decorator
import inspect
@decorator
def wrapper(func, self, *args, **kwargs):
specs = inspect.getargspec(func)
# Convert parameters
new_args = list(args)
for ind, name in enumerate(specs.args[1:]):
if name == "data":
new_args[ind] = _convert_data(new_args[ind])
elif name == "params":
new_args[ind] = _convert_params(new_args[ind])
elif name == "interval":
new_args[ind] = _convert_interval(new_args[ind])
return func(self, *new_args, **kwargs)
And the usage is much simpler then. The only important thing is keep using the same names for arguments between different functions.
class TheClass():
@convertparameters
def func1(self, data, params, interval):
....
@convertparameters
def func2(self, data, params):
....