Search code examples
pythonparameter-passingdecoratorkeyword-argument

python decorator to check for already called func with unique arguments


I m writing python decorator to check if func was previously called with same arguments.

Below is proof of concept code

storage = list()
def f(*args, **kwargs):

    s = ''
    for i in range(len(args)):
        s += str(args[i])
    kv = ''
    for k, v in kwargs.items():
        kv += str(k) + str(v)

    main = s+kv
    if main not in storage:
        storage.append(main)
    else:
        print('this was called!')
    print('printing storage')
    print(storage)



if __name__ == '__main__':


    f(1, 2, 3, 4, 5, 6, 7, 8, x=10)
    f(1, 2, 3, 4, 5, 6, 7, 8, x=10, z=10)
    f(1, 2, 3, 4, 5, 6, 7, 8, x=10, z=10) #this combination of args, kwargs should be skipped by the function f

My actual decorator fails with this error msg:

"TypeError('can only concatenate tuple (not "dict") to tuple')"

challenge website link

here i simply turn the list of args into str and **kwargs dict into string and concatenate them to create unique combination of args/kwargs called and store it in storage list

e.g. func(1,2,3,x=3,b=4) => 123x3b4

class Answer:
    def RepeatDecorator(self, func):
        self.storage = list()
        def wrapper(*args, **kwargs):
            s = ''
            for i in range(len(args)):
                s += str(args[i])
            kv = ''
            for k, v in kwargs.items():
                kv += str(k) + str(v)

            main = s+kv
            if main not in self.storage:
                self.storage.append(main)
                func(args, kwargs)
            else:
                print("func with this args was already called, do nothing")                                        
        return wrapper

e.g.

func1(1,x=2) func1(1,x=3,b=4) func2(1,x=2) func2(1,x=3,b=4

all 4 should work and be stored, since func1 and func2 are different functions


Solution

  • Here are some issues I found with your code:

    • Different functions given to Answer.RepeatDecorator will share the same storage list, if they're using the same Answer instance.
    • Pasting strings together is ambiguous. This will treat func(12, 3) the same as func(1, 23) and func(x=4) the same as func('x4').
    • Looking up strings in a list is fairly slow. In this case it's unlikely to be a problem, but you'll probably want to use a set if this kinda thing is needed in the future (as long as the arguments are hashable).
    • You're calling func(args, kwargs) when you should call func(*args, **kwargs). It's something I often overlook when writing decorators.
    class Answer:
        def RepeatDecorator(self, func):
            # Use a local variable rather than an attribute on self
            storage = []
            def wrapper(*args, **kwargs):
                key = (args, kwargs)
                if key not in storage:
                    storage.append(key)
                    # It doesn't say you need to return the value func returns, but it can't hurt, right?
                    return func(*args, *kwargs)
                print("func with this args was already called, do nothing")                                        
            return wrapper
    

    For completeness sake, here's how I would do it using a set:

    class Answer:
        def RepeatDecorator(self, func):
            storage = set()
            def wrapper(*args, **kwargs):
                key = (args, tuple(kwargs.items()))
                if key not in storage:
                    storage.add(key)
                    return func(*args, *kwargs)
                print("func with this args was already called, do nothing")                                        
            return wrapper