Cache for general functions

Cache for general functions#

Another way of looking at *args is to say that it represents a general function signature, in the sense that it represents all functions that accept positional arguments. For the function fibonacci, *args boils down to a single value; and for the function combinations_len, *args will represent the two arguments required.

So, to fix the decorator cached_factory, the inner function cached_f must accept a variable number of arguments with *args:

def cached_factory(f):
    cache = {}
    def cached_f(*args):
        ...  # The rest of the code goes here...

Now, args is a tuple, so you can use it as the key to your dictionary cache:

def cached_factory(f):
    cache = {}
    def cached_f(*args):
        if args not in cache:
            cache[args] = f(...)  # ?!
        return cache[args]

    return cached_f

But how do you take the tuple args and pass it to the original function f? The function f may accept one argument, or two, or twenty, but right now we have all of its arguments in a tuple called args… However, the syntax that Python uses to collect a variable number of arguments into a tuple, *args, can also be used to unpack a tuple of arguments into the distinct arguments of a function.

Putting everything together, the improved version of the decorator cached_factory looks like this:

def cached_factory(f):
    cache = {}
    def cached_f(*args):
        if args not in cache:
            cache[args] = f(*args)  # !!
        return cache[args]

    return cached_f

This decorator can be used in functions that take no arguments, a single argument, two arguments, and more!

from itertools import combinations

@cached_factory
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

@cached_factory
def combinations_len(n, r):
    return len(list(combinations(range(n), r)))

print(fibonacci(40))  # 102334155
print(combinations_len(30, 6))  # 593775