python decorator to check for already called func with unique arguments

Question:

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

Asked By: ERJAN

||

Answers:

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
Answered By: Jasmijn