Introduction

Python's standard library comes with a memoization function in the functools module named @functools.lru_cache. This can be very useful for pure functions (functions that always will return the same output given an input) as it can be used to speed up an application by remembering a return value. This is because next time a function is called with the same arguments, the value can be simply returned and computation will not have to occur again.

Sometimes though, you might want to remember the return value of a non-pure function for a specific amount of time. The function below is a decorator that allows you to remember return value but also have an expiry on them.

Decorator

def cache(size_limit=0, ttl=0, quick_key_access=False):
    def decorator(func):
        import time
        storage = {}
        ttls = {}
        keys = []

        def wrapper(*args, **kwargs):
            # Generate a key based on arguments being passed
            key = (*args,) + tuple([(k, v) for k, v in kwargs.items()])

            # Check if they return value is already known
            if key in storage:
                result = storage[key]
            else:
                # If not, get the result
                result = func(*args, **kwargs)
                storage[key] = result

                # If a ttl has been set, remember when it is going to expire
                if ttl != 0:
                    ttls[key] = time.time() + ttl

                # If quick_key_access is being used, remember the key
                if quick_key_access:
                    keys.append(key)

                # If a size limit has been set, make sure the size hasn't been exceeded
                if size_limit != 0 and len(storage) > size_limit:
                    if quick_key_access:
                        oldest_key = keys[0]
                        del keys[0]
                    else:
                        oldest_key = list(storage.keys())[0]
                    del storage[oldest_key]

            # Check ttls if it is enabled
            if ttl != 0:
                while True:
                    if quick_key_access:
                        oldest_key = keys[0]
                    else:
                        oldest_key = list(storage.keys())[0]

                    # If they key has expired, remove the entry and it's quick access key if quick_key_access=True
                    if ttls[oldest_key] < time.time():
                        del storage[oldest_key]
                        if quick_key_access:
                            del keys[0]
                    else:
                        break

            return result
        return wrapper
    return decorator

Usage

To use this method, add it to your project/script. Then decorate your functions that you want to cache, for example:

@cache
def my_sum(a, b, c, d=1, e=5):
    return a + b + c + d + e

Now when you call my_sum(...) values will be remembered and the addition will not have to occur each time. In this example, all the defaults are used which means it has unlimited storage (can be dangerous) and will live forever; this is the same as memoization will unlimited entries.

The first parameter for the cache function is called size_limit. This is the maximum amount of entries to be stored until values that have been stored for the longest are removed (only the oldest will be deleted in Python 3.7 and above due to the ordering of dictionaries). When this is set to 0, there is no size limit.

ttl is the second parameter for this decorator. This is the number of seconds that each entry can exist for before it is removed from memory. When this is set to 0 entries will never expire.

There is also another parameter that can be passed called quick_key_access. When set to true, a lookup table is stored for each key that is generated. This is set to false by default but if you're remembering a lot of values, then this may help get a speed boost at the cost of taking up more space. Simply, when set to false, this is slower but will take up less space, when set to true, this is faster but will take up more space.

An example of using this decorator with a size limit of 10 cached entries, a time to live of 5min and using quick key access is shown below:

@cache(10, 60 * 5, True)
def my_sum(a, b, c, d=1, e=5):
    return a + b + c + d + e

Summary

If you want a simple memoization function, I still recommend using the built-in @functools.lru_cache decorator but if you are looking for memoization decorator with a time factor added in, then this is something that you're looking for.