Search code examples
pythondecoratormemoization

Identifying equivalent varargs function calls for memoization


I'm using a variant of the this decorator for memoization:

# note that this decorator ignores **kwargs
def memoize(obj):
    cache = obj.cache = {}

    @functools.wraps(obj)
    def memoizer(*args, **kwargs):
        if args not in cache:
            cache[args] = obj(*args, **kwargs)
        return cache[args]
    return memoizer

I'm wondering, is there a reasonable way to memoize based on both args and kwargs, particularly in cases where two function calls specified with arguments assigned differently positionally and through keyword, but have the exact same arguments?


Solution

  • If you are using parameters either always as positionals or always as keywords, Thorsten solution works fine. But, if you want to consider equal calls that give to the parameters the same values, indipendently of how the parameters are passed, then you have to do something more complex:

    import inspect
    
    
    def make_key_maker(func):
        args_spec = inspect.getargspec(func)
    
        def key_maker(*args, **kwargs):
            left_args = args_spec.args[len(args):]
            num_defaults = len(args_spec.defaults or ())
            defaults_names = args_spec.args[-num_defaults:]
    
            if not set(left_args).symmetric_difference(kwargs).issubset(defaults_names):
                # We got an error in the function call. Let's simply trigger it
                func(*args, **kwargs)
    
            start = 0
            key = []
            for arg, arg_name in zip(args, args_spec.args):
                key.append(arg)
                if arg_name in defaults_names:
                    start += 1
    
            for left_arg in left_args:
                try:
                    key.append(kwargs[left_arg])
                except KeyError:
                    key.append(args_spec.defaults[start])
    
                # Increase index if we used a default, or if the argument was provided
                if left_arg in defaults_names:
                    start += 1
            return tuple(key)
    
        return key_maker
    

    The above functions tries to map keyword arguments(and defaults) to positional and uses the resultant tuple as key. I tested it a bit and it seems to work properly in most cases. It fails when the target function also uses a **kwargs argument.

    >>> def my_function(a,b,c,d,e=True,f="something"): pass
    ... 
    >>> key_maker = make_key_maker(my_function)
    >>> 
    >>> key_maker(1,2,3,4)
    (1, 2, 3, 4, True, 'something')
    >>> key_maker(1,2,3,4, e=True)               # same as before
    (1, 2, 3, 4, True, 'something')
    >>> key_maker(1,2,3,4, True)                 # same as before
    (1, 2, 3, 4, True, 'something')
    >>> key_maker(1,2,3,4, True, f="something")  # same as before
    (1, 2, 3, 4, True, 'something')
    >>> key_maker(1,2,3,4, True, "something")    # same as before
    (1, 2, 3, 4, True, 'something')
    >>> key_maker(1,2,3,d=4)                     # same as before
    (1, 2, 3, 4, True, 'something')
    >>> key_maker(1,2,3,d=4, f="something")      # same as before
    (1, 2, 3, 4, True, 'something')