UPDATE - Oct 27, 2017 This snippet did now become its own PyPI package. See https://pypi.python.org/pypi/django-cache-memoize
This is something that's grown up organically when working on Mozilla Symbol Server. It has served me very well and perhaps it's worth extracting into its own lib.
Usage
Basically, you are probably used to this in Django:
from django.core.cache import cache
def compute_something(user, special=False):
cache_key = 'meatycomputation:{}:special={}'.format(user.id, special)
value = cache.get(cache_key)
if value is None:
value = _call_the_meat(user.id, special) # some really slow function
cache.set(cache_key, value, 60 * 5)
return value
Here's instead how you can do exactly the same with cache_memoize
:
from wherever.decorators import cache_memoize
@cache_memoize(60 * 5)
def compute_something(user, special=False):
return _call_the_meat(user.id, special) # some really slow function
Cache invalidation
If you ever need to do non-trivial caching you know it's important to be able to invalidate the cache. Usually, to be able to do that you need to involved in how the cache key was created.
Consider our two examples above, here's first the common thing to do:
def save_user(user):
do_something_that_will_need_to_cache_invalidate(user)
cache_key = 'meatycomputation:{}:special={}'.format(user.id, False)
cache.delete(cache_key)
# And when it was special=True
cache_key = 'meatycomputation:{}:special={}'.format(user.id, True)
cache.delete(cache_key)
This works but it involves repeating the code that generates the cache key. You could extract that into its own function of course.
Here's how you do it with the cache_memoize
decorator:
def save_user(user):
do_something_that_will_need_to_cache_invalidate(user)
compute_something.invalidate(user, special=False)
compute_something.invalidate(user, special=True)
Other features
There are actually two ways to "invalidate" the cache. Calling the new myoriginalfunction.invalidate(...)
function or passing a custom extra keyword argument called _refresh
. For example: compute_something(user, _refresh=True)
.
You can pass callables that get called when the cache works in your favor or when it's a cache miss. For example:
def increment_hits(user, special=None):
# use your imagination
metrics.incr(user.email)
def cache_miss(user, special=None):
print("cache miss on {}".format(user.email))
@cache_memoize(
60 * 5,
hit_callable=increment_hits,
miss_callable=cache_miss,
)
def compute_something(user, special=False):
return _call_the_meat(user.id, special) # some really slow function
Sometimes you just want to use the memoizer to make sure something only gets called "once" (or once per time interval). In that case it might be smart to not flood your cache backend with the value of the function output if there is one. For example:
@cache_memoize(60 * 60, store_result=False) # idempotent guard
def calculate_and_update(user):
# do something expensive here that is best to only do once per hour
Internally cache_memoize
will basically try to convert every argument and keyword argument to a string with, kinda, str()
. That might not always be appropriate because you might know that you have two distinct objects whose __str__
will yield the same result. For that you can use the args_rewrite
parameter. For example:
def simplify_special_objects(obj):
# use your imagination
return obj.hostname
@cache_memoize(60 * 5, args_rewrite=simplify_special_objects)
def compute_something(special_obj):
return _call_the_meat(special_obj.hostname)
In conclusion
I've uploaded the code as a gist.
It's quite possible that there's already a perfectly good lib that does exactly this. If so, thanks for letting me know. If not, perhaps I ought to wrap this up and publish it on PyPI. Again, that's for letting me know.
UPDATE
I found a bug in the original gist. Updated 2017-10-05.
The bug was that the calling of miss_callable
and hit_callable
was reversed.
Comments
Please add on PyPI (with a license, too!). That way if/when bugs are found people will get the updates instead of copy-pasting your gist...
See https://pypi.python.org/pypi/django-cache-memoize :)
Thx for creating this!!! Totally helpful.
okok