How to use Python3 decorator to reuse functions instead of modifying itself?

Viewed 66

I have some functions in a class that are used for calculations. These functions all take a single argument, period, which is required to be int.

I want to use a decorator so that when I enter List[int] for that argument, it can be interpreted as taking that value as an argument and executing the function for all values in that list.

Since this decorator may be used on other functions with keyword arguments, I have to include *kwargs in the decorator input. I used the following reference code:

from typing import List

def multiple_para(func):
    def wrapper(_, period, *args, **kw):  # Since the first argument must be self, use _ instead
        results = []
        if isinstance(period, List):
            for i in period:
                result.append(func(_, period=i, *args, **kw))
        else:
            result = func(_, period=period, *args, **kw)
        return result
    return wrapper

class Test():
    @multiple_para
    def test(self, period: int, target: str = ''):
        output = period + 1  # substitute calculation process
        return output

if __name__ == '__main__':
    test1 = Test()
    test1.test(period=[1, 2, 3])

However, I get the following error message:

TypeError: test() got multiple values for argument 'period'

2 Answers

This part won't work as you expect.

for i in period:
    return func(_, period=i, *args, **kw)

In for loop return will be called only once for the first value only.

As you are receiving a list as input it's only logical to return a list as well.

Something like this should work instead

result = []
for i in period:
    result.append(func(_, period=i, *args, **kw))
return result

or a shorter version

return [func(_, period=value, *args, **kw) for value in period]

I think it's conceptually sound, as you're avoiding duplicate logic on each function that may receive a scalar or a list. It would also make it easier to evolve the code later to work with other collections.

What you're doing in the end is a functional composition, like applying g(f(x)) in math which is perfectly fine, a technique used extensively on functional programming and at the end, the purpose of the decorators.

There's just a little bug in the implementation as:

return func(_, period=i, *args, **kw)

Will apply the function only to the first element of the collection and then return the result, I believe you'd like to return a list with all results instead.

Also, you may want to use functools.wraps() to avoid some boiler plate when defining decorators.

An (older-style) alternative was to have a class with the method making the distinction between collection and scalar and calling an abstract method, and having this abstract method implemented in classes inheriting from it, but this was cumbersome and created an "Is-A" relationship between the base class and the ones inheriting it that was artificial. So overall this is a better approach.

Related