PythonMastery
Python programs · Useful programs · intermediate

Memoization in Python without a library

Write your own memoize decorator with a dictionary, and see it cut a recursive Fibonacci function from thousands of calls to a few.

Memoization means saving the result of a function call, so the next call with the same arguments returns the saved result instead of working it out again. Here you build it yourself with a dictionary and a decorator.

The program

python
def memoize(func):
    cache = {}

    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper


calls = 0


def slow_fib(n):
    global calls
    calls += 1
    return n if n < 2 else slow_fib(n - 1) + slow_fib(n - 2)


print("plain:   ", slow_fib(20), "after", calls, "calls")

calls = 0


@memoize
def fast_fib(n):
    global calls
    calls += 1
    return n if n < 2 else fast_fib(n - 1) + fast_fib(n - 2)


print("memoized:", fast_fib(20), "after", calls, "calls")
print("again:   ", fast_fib(20), "after", calls, "calls")
+ setup added so this can run · defines args
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

args = _AutoMock('args')
output
plain:    6765 after 21891 calls
memoized: 6765 after 21 calls
again:    6765 after 21 calls

How it works

Limits of this version

The standard library version, functools.cache (or functools.lru_cache(maxsize=...) with a size limit), handles keyword arguments and size limits. Use it in real code. Writing your own once shows what it does.

Related programs: Memoization with functools.cache.

Learn the idea: Decorators: Functions That Wrap Functions, Dictionaries