PythonMastery

Cache a slow pure function with one decorator

@functools.cache remembers a function's results by its arguments, so repeated calls return instantly. Count the calls before and after, and know when not to.

If a function always returns the same result for the same arguments, and it's slow, there's no reason to work the answer out twice. functools.cache stores each result the first time and hands it back from then on.

Before

A recursive Fibonacci recomputes the same values over and over. Count how many calls it makes:

python
calls = 0

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

print(fib(25), "in", calls, "calls")
output
75025 in 242785 calls

After

python
from functools import cache

calls = 0

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

print(fib(25), "in", calls, "calls")
print(fib(25), "in", calls, "calls")   # asked again: answered from the cache
print(fib.cache_info())
output
75025 in 26 calls
75025 in 26 calls
CacheInfo(hits=24, misses=26, maxsize=None, currsize=26)

From 242,785 calls to 26: each value from 0 to 25 is worked out once.

Put a limit on it

cache keeps every result forever. For a function called with many different arguments, lru_cache(maxsize=...) keeps only the most recently used ones.

python
from functools import lru_cache

@lru_cache(maxsize=2)
def exchange_rate(currency):
    print("  looking up", currency)
    return {"EUR": 1.17, "USD": 1.33, "INR": 111.5}[currency]

for c in ["EUR", "EUR", "USD", "INR", "EUR"]:
    exchange_rate(c)
print(exchange_rate.cache_info())
output
  looking up EUR
  looking up USD
  looking up INR
  looking up EUR
CacheInfo(hits=1, misses=4, maxsize=2, currsize=2)

With room for two, looking up INR pushed EUR out, so the last EUR was fetched again.

Why it works

The decorator wraps the function in a dict keyed by the arguments. A call with arguments it has seen skips the body entirely.

When not to use it

Only on pure functions: same arguments, same answer, no side effects. Cache a function that reads the clock, a file or a database and it will keep returning the old answer. Arguments must be hashable, so a list argument raises TypeError: unhashable type. And an unbounded cache on a function called with endless distinct arguments is a memory leak; that's what maxsize is for.

Learn it properly: Decorators: Functions That Wrap Functions, Performance: Profile, Then Optimise