Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pythonic way of converting parameters to the same standard within all methods of a class

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):
        ....
like image 655
Vladimir Avatar asked Sep 16 '15 09:09

Vladimir


People also ask

Do all class methods need self?

Class methods don't need a class instance. They can't access the instance ( self ) but they have access to the class itself via cls . Static methods don't have access to cls or self . They work like regular functions but belong to the class's namespace.

What does __ class __ mean in Python?

__class__ is an attribute on the object that refers to the class from which the object was created. a. __class__ # Output: <class 'int'> b. __class__ # Output: <class 'float'> After simple data types, let's now understand the type function and __class__ attribute with the help of a user-defined class, Human .

Can you have two methods with the same name in Python?

In python, unlike other languages, you cannot perform method overloading by using the same method name. Why? Everything is an object in python, classes, and even methods. Say you have an object Addition, which is a class (everything in python is an object, so the class Addition is an object too).

What is __ INT __ in Python?

The __int__ method is called to implement the built-in int function. The __index__ method implements type conversion to an int when the object is used in a slice expression and the built-in hex , oct , and bin functions.


2 Answers

  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.

like image 184
bagrat Avatar answered Nov 15 '22 00:11

bagrat


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

like image 32
zmbq Avatar answered Nov 14 '22 22:11

zmbq