Caching behind an interface
When a lookup is slow, wrap it rather than editing it. A caching wrapper has the same methods as the real implementation, holds it, and answers repeated requests from memory. Callers cannot tell the difference, and the slow code stays simple.
class SlowPrices:
def price(self, sku):
print("asking the slow service")
return 3
class CachedPrices:
def __init__(self, inner):
self.inner = inner
self.cache = {}
def price(self, sku):
if sku not in self.cache:
p = self.inner.price(sku)
self.cache[sku] = p
return self.cache[sku]
cached = CachedPrices(SlowPrices())
print(cached.price("A"), cached.price("A"))Expiry needs a clock
Cached data goes stale, so give each entry an age limit. Do not read the system clock inside the cache: take a clock as a parameter, a function returning seconds. The application passes time.monotonic; a test passes a fake it can move forward, and expiry becomes a plain assertion instead of a wait.
Your task
Write CachedPrices(inner, ttl, clock) whose price(sku) calls inner.price(sku) only when there is no entry for sku or the entry is at least ttl seconds old by clock(), and whose invalidate(sku) drops one entry, ignoring unknown skus. A price of 0 is cached like any other.
The explanation opens here once every check passes.