6

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:

  1. 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)
  2. 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
  3. 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):
        ....
Vladimir
  • 1,363
  • 2
  • 14
  • 28

2 Answers2

3
  1. I would agree with you, that this is a better solution, so nothing to do here.
  2. I think you will come up with a good order and will keep it further, which will not become a problem in future.
  3. This is a problem, you are right, but it is being solved easily with functools.wraps. Simply decorate your newfunc with it, and you will save the signature of the original function.

    from functools import wraps
    
    def convertparameters(*types):
        def wrapper(func):
            @wraps(func)
            def new_func(self, *args, **kwargs):
                pass # Your stuff
    

    Tough, this works only for Python 3. In Python 2, the signature would not be preserved, only __name__ and __doc__ would be. So in case of Python, you could use the decorator module:

    from decorator import decorator
    
    def convertparameters(*types):
        @decorator
        def wrapper(func, self, *args, **kwargs):
            pass  # return a result
        return wrapper
    

EDIT based on user3160867's update.

Community
  • 1
  • 1
bagrat
  • 7,158
  • 6
  • 29
  • 47
  • Thanks, I did not know about `wraps`. I assume there should be also `return new_func` and `return wrapper`? – Vladimir Sep 16 '15 at 15:43
  • I have one more question: `wraps` correctly transfers the docstring. However the signature stays `func1(*args, **kwargs)`. Are there any way to transfer signature as well? – Vladimir Sep 16 '15 at 15:45
  • I guess you are using Python 2. In case of Python 3 the signature also would be preserved. Check out my update. – bagrat Sep 16 '15 at 17:22
  • the variant with `@decorator` does not work (see e.g. http://pastebin.com/9D8N1Xgy) - in this form it rises `TypeError` (new_func() takes exactly 1 argument (xxx given)) - it should be decorating `wrapper` – Vladimir Sep 16 '15 at 20:38
  • In fact, this way it will not work either :) The `wrapper` should return not a function but a result here. I've updated the question with the solution - you could just copy the signature from it and I'll accept your answer – Vladimir Sep 17 '15 at 07:27
  • I was supposed to answer your question, but you have done all the job :) – bagrat Sep 17 '15 at 09:56
1

To make the code a bit more concise, don't provide arguments to the decorator - deduce them from the decorated function.

zmbq
  • 38,013
  • 14
  • 101
  • 171
  • That sounds interesting! Do you mean declaring `def wrapper(func, self, **kwargs)` without `args` and inferring from the `kwargs`? – Vladimir Sep 18 '15 at 10:42
  • No, you can use this: http://stackoverflow.com/questions/218616/getting-method-parameter-names-in-python – zmbq Sep 19 '15 at 21:25