I recently played around with a script that got some data from the Google API. As I didn't want to spam requests at the service (and potentially get blocked), I made this decorator, which caches the result of a function for a specified amount of time. Any call after the time-to-live (TTL) will call the function again.
This is the first decorator I wrote that takes an optional argument (the time to keep the cache). That code was taken from this StackOverflow answer by @Eric. I also couldn't abstain from using the new walrus operator (Python 3.8+), since I'm always looking for opportunities to use it in order to get a better feel for it.
Any and all advice on how to make this better or more readable are welcome.
from datetime import datetime
from functools import wraps
DEBUG = True
def temporary_cache(*args, ttl=60):
"""A decorator that ensures that the result of the function call
is cached for `ttl` seconds (default: 60).
Warning: The returned object is stored directly, mutating it also mutates the
cached object. Make a copy if you want to avoid that.
"""
def decorator(func):
func.cache = None
func.cache_time = datetime.fromordinal(1)
@wraps(func)
def inner(*args, **kwargs):
if ((now := datetime.now()) - func.cache_time).total_seconds() > ttl:
func.cache = func(*args, **kwargs)
func.cache_time = now
elif DEBUG:
# for debugging, disable in production
print("Cached", func.__name__)
return func.cache
return inner
if len(args) == 1 and callable(args[0]):
return decorator(args[0])
elif args:
raise ValueError("Must supply the decorator arguments as keywords.")
return decorator
Example usages:
import time
@temporary_cache
def f():
return datetime.now()
@temporary_cache(ttl=1)
def g():
return datetime.now()
if __name__ == "__main__":
print(f())
# 2020年05月12日 10:41:18.633386
time.sleep(2)
print(f())
# Cached f
# 2020年05月12日 10:41:18.633386
print(g())
# 2020年05月12日 10:41:20.635594
time.sleep(2)
print(g())
# 2020年05月12日 10:41:22.636782
Note that f was still cached, while g was not, because the TTL is shorter than the time between calls.
1 Answer 1
Rather than using
*argsyou can supply a default positional only argument.def temporary_cache(fn=None, *, ttl=60): ... if fn is not None: return decorator(fn) return decoratorIf you feel following "flat is better than nested" is best, we can use
functools.partialto remove the need to definedecorator.def temporary_cache(fn=None, *, ttl=60): if fn is None: return functools.partial(temporary_cache, ttl=ttl) @functools.wraps(fn) def inner(*args, **kwargs): ...for debugging, disable in production
You can use
loggingfor this. I will leave actually implementing this as an exercise.I also couldn't abstain from using the new walrus operator (Python 3.8+), since I'm always looking for opportunities to use it in order to get a better feel for it.
A very reasonable thing to do. Abuse the new feature until you know what not to do. +1
However, I don't think this is a good place for it. Given all the brackets in such a small space I'm getting bracket blindness. I can't tell where one bracket ends and the others starts.
I am not a fan of
func.cache = ...andfunc.cache_time. You can stop assigning to a function by usingnonlocal.
Bringing this all together, and following some of my personal style guide, gets the following. I'm not really sure which is better, but it's food for thought.
from datetime import datetime
import functools
def temporary_cache(fn=None, *, ttl=60):
if fn is None:
return functools.partial(temporary_cache, ttl=ttl)
cache = None
cache_time = datetime.fromordinal(1)
@functools.wraps(fn)
def inner(*args, **kwargs):
nonlocal cache, cache_time
now = datetime.now()
if ttl < (now - cache_time).total_seconds():
cache = fn(*args, **kwargs)
cache_time = now
elif DEBUG:
# for debugging, disable in production
print("Cached", fn.__name__)
return cache
return inner
-
\$\begingroup\$ I was playing around with making
ttlkeyword only, but when preceeded by*argsit insisted onargshaving to be empty(?). This is a nice way around it. I also previously hadnonlocal, but likedfunc.cachemore because then it is possible to get the value from outside. Didn't know you could assign multiple variablesnonlocalat once, though! \$\endgroup\$Graipher– Graipher2020年05月12日 13:36:03 +00:00Commented May 12, 2020 at 13:36 -
1\$\begingroup\$ @Graipher That sounds strange, I really don't know what was going on there :O I'm not entirely sure why you want to get
func.cachefrom the outside, but me understanding isn't really important :) However since you do, might I suggestinner.cacheoverfunc.cache. \$\endgroup\$2020年05月12日 13:43:41 +00:00Commented May 12, 2020 at 13:43 -
\$\begingroup\$ Well, whenever I was using caching decorators in the past, there came the time (during development) where I needed to have a look what was actually in the cache, so it's just convenience, I guess :D \$\endgroup\$Graipher– Graipher2020年05月12日 14:16:54 +00:00Commented May 12, 2020 at 14:16
Explore related questions
See similar questions with these tags.
cacheandcache_timedictionaries of the arguments. \$\endgroup\$