Caching Properties in Python

1 minute read

Updated:

What are cached properties and why do I care?

A cached property is a mechanism that allows for storing the result of a function call, so that future calls return the cached value and avoid computation costs. If the function is expensive, this can save on time and API calls.

How is this done?

You can certainly implement a naive solution yourself.

cached_value = None
def foo():
    if cached_value:
        return cached

There are exisitng packages that can do this and with more functionality. Let’s talk about these.

cached-property

cached-property is a python package that pulled out the caching functionality from django.

When to use

cached-property should be used if you need to cache the result of a function call that does not take parameters (at least changing parameters). It is good for functions that are slow-changing but changing, nonetheless. This will give you singleton-ish behaviour. It has support for threading, a way to invalidate the cache, and TTL settings.

How to use

from cached_property import cached_property

@cached_property
def get_user_preference():
  # Super expensive query to get user preference
  pass

User preferences are slow changing but is accessed frequently. A common coding paradigm is to store a local copy after making the expensive call. This introduces code noise because you’ll have an extra local variable, or you would need to modify the function call like above. Using cached-property takes care of all these details.

We can invalidate the cache every 5 minutes, which means that user preferences are stale for a maximum of 5 minutes. To refresh, it’s the same function call but returns a different result because the function is non-deterministic (changes over time depending on the data on backend).

functools.lru_cache

Python 3.2 introduces functools.lru_cache decorator that will allows you cache function calls, limiting the cache size using an LRU cache.

When to use

functools.lru_cache should be used when you need to cache the results of a deterministic function call for memoization purposes. Think the classic fibonnaci sequence, this is the perfect use case.

How to use

from functools import lru_cache
@lru_cache(maxsize=1000)
def fibonacci(n):
  if n == 0 or n == 1:
    return 1
  else:
    return fibonacci(n-1) + fibonacci(n-2)

If we run fibonacci(100), we will recurse and cache the results for 0 < n < 100. Any subsequent function call within that range will return the cached result instead of recursing.

Updated: