Durable memoization that automatically refreshes as you update your source code.
Requires Python >= 3.9.
python3 -m pip install astrocache
This library provides disk-backed memoization for Python functions with a surprisingly useful twist: the cache is sensitive not only to your function's inputs, but also to its implementation (represented by its abstract syntax tree or AST).
If you're unfamiliar with memoization, check out the Wikipedia page and functools.cache for in-depth explanations.
import astrocache
@astrocache.cache()
def foo(a, b):
return slow_fn(a) + bThis creates a disk-backed cache for foo, which prevents foo from performing the
same work more than once for a given input. So far, this is pretty standard; many
other libraries offer the same thing.
But what if you change the behavior of foo?
@astrocache.cache()
def foo(a, b):
return slow_fn(a) + b * 2Your cache entries are no longer valid.
Luckily, astrocache is aware of this. It will create new cache entries for the new
version of foo.
This treatment extends recursively to any function called by foo as well. In this
case, changes to slow_fn (and to functions called by slow_fn, etc) will also
invalidate cache entries for foo.
Astrocache creates a fingerprint of your function's abstract syntax tree (AST) using the ast module from the standard library.
When your function is called, this fingerprint is combined with the provided arguments to create a cache key.
This is particularly useful in highly interactive workflows, e.g. during rapid iteration or in a notebook setting. Many libraries provide some form of memoization, but none (as far as I know) manage this kind of cache invalidation for you.
As an example, imagine you're rapidly iterating on a script that processes data in several expensive steps, or hits a usage-capped external API.
Memoization could certainly make you more productive, but you'd have to remember to clear the various cache entries as you updated your code.
This library automates this for you, allowing you to take advantage of memoization without needing to worry about clearing the cache.
There is a caveat regarding AST inspection of functions that are referenced, but not invoked. Example:
import astrocache
def bar(a):
return a + 1
@astrocache.cache()
def foo(x):
return baz(bar)Here, bar is referenced (passed as an argument), but not invoked, by foo.
Functions appearing within your cached function are only included in the AST fingerprint if your cached function does any of these:
- Calls the referenced function
- Receives the referenced function as an argument
- Contains the definition of the referenced function
Here are some examples to illustrate this:
✅ Calling a function
@astrocache.cache()
def foo(referenced_function):
# referenced_function will be included
referenced_function(1)✅ Receiving the function as an argument
@astrocache.cache()
def foo(referenced_function):
# referenced_function will be included
foo(referenced_function)✅ Defining a function within the cached function
@astrocache.cache()
def foo():
# referenced_function will be included
def referenced_function():
return 1
foo(referenced_function)❌ Assigning a function from outer scope to a variable (only)
@astrocache.cache()
def foo():
# referenced_function will NOT be included
foo = referenced_function❌ Passing a function from outer scope to a called function (only)
@astrocache.cache()
def foo():
# referenced_function will NOT be included
foo(referenced_function)