Implementing TTL Cache in Python (2)
Overview
Continuing from our last article, we’ll delve deeper into implementing a cache with Time-to-Live (TTL).
Let’s quickly recap what we covered previously.
Summary of Part 1
- We learned how to equip functions with cache capabilities using
lru_cache
. - We implemented the
get_ttl_hash
function, which updates return values at specified intervals. - We were able to give functions TTL caching by:
- Enhancing them with the caching functionality of
lru_cache
. - Adding a dummy argument.
- Inputting the return value of
get_ttl_hash
to the dummy argument.
- Enhancing them with the caching functionality of
Our goal in this article is to create a decorator that automates this process.
Implement TTL Cache Decorator
Let’s move on to implementing a TTL cache decorator.
Understanding Decorators
First off, let’s briefly explain what decorators are.
Simply put, a decorator is a function that takes another function as an input and returns a new function.
The following two examples get the same output:
-
Using the
lru_cache
decorator:from functools import lru_cache @lru_cache def fibonacci(n): ... print(fibonacci(10))
-
Directly calling and using
lru_cache
as a function:from functools import lru_cache def fibonacci(n): ... print(lru_cache(fibonacci)(10))
Implementation
First, let’s organize what we want to achieve.
Start with the end in mind.
By adding @ttl_cache(ttl_seconds=60)
at the beginning of a function,
you equip that function with a cache feature that lasts for 60 seconds.
@ttl_cache(ttl_seconds=60)
def myfunc(a, b):
...
- Once executed, the computed result is stored as a cache.
- Once the cache expiration period (60 seconds in the above example) elapses,
the value is recalculated and the cache is updated upon the next execution.
In other words…
The ttl_cache
essentially becomes a function that:
- Takes an expiration time (
ttl_seconds
) as input and returns a decorator with the following functionality. - Takes a function as input and returns a modified function with TTL caching capabilities.
So, we’re essentially implementing a function that returns a decorator.
Function That Returns a Decorator
Given the expiration duration ttl_seconds
as an argument, the function returning the decorator looks like this:
def ttl_cache(ttl_seconds=3600):
def ttl_cache_deco(func):
...
return ttl_cache_deco
Next, we’ll define the inner ttl_cache_deco
decorator.
Decorator Adding TTL Caching Functionality
Now, we’ll define a decorator that, when given a function, returns the function with TTL caching capabilities.
This can be achieved using the following steps:
- Create a function with added caching capabilities and a dummy argument.
- Compute
get_ttl_hash(ttl_seconds)
and use it as an input for the dummy argument in the function.
def ttl_cache_deco(func):
"""Returns a function with time-to-live (TTL) caching capabilities."""
# Function with caching capability and dummy argument
@lru_cache(maxsize=None)
def cached_dummy_func(*args, ttl_dummy, **kwargs):
del ttl_dummy # Remove the dummy argument
return func(*args, **kwargs)
# Function to input the hash value into the dummy argument
def ttl_cached_func(*args, **kwargs):
hash = get_ttl_hash(ttl_seconds)
return cached_dummy_func(*args, ttl_dummy=hash, **kwargs)
return ttl_cached_func
When applying the above decorator to a function, as evident from the code,
the original function gets transformed into the inner ttl_cached_func
.
In this transformation, we lose the metadata (arguments information, docstrings, …) of the original function.
To prevent this, we use the functools.wraps
decorator on the function returned by the decorator.
(Refer to: functools.wraps)
from functools import lru_cache, wraps
...
def ttl_cache_deco(func):
...
@wraps(func)
def ttl_cached_func(*args, **kwargs):
...
By applying the @wraps
decorator this way, we ensure the conversion of the function without losing its original metadata.
Last Code
With that, our decorator that adds a time-to-live cache functionality is complete.
Here’s a recap of the completed ttl_cache
decorator implementation.
import datetime
from functools import lru_cache, wraps
def get_ttl_hash(seconds=3600):
"""Calculate hash value for TTL caching.
Args:
seconds (int, optional): Expiration time in seconds. Defaults to 3600.
Returns:
int: Hash value.
"""
utime = datetime.datetime.now().timestamp()
return round(utime / (seconds + 1))
def ttl_cache(ttl_seconds=3600):
"""A decorator for TTL cache functionality.
Args:
ttl_seconds (int, optional): Expiration time in seconds. Defaults to 3600.
"""
def ttl_cache_deco(func):
"""Returns a function with time-to-live (TTL) caching capabilities."""
# Function with caching capability and dummy argument
@lru_cache(maxsize=None)
def cached_dummy_func(*args, ttl_dummy, **kwargs):
del ttl_dummy # Remove the dummy argument
return func(*args, **kwargs)
# Function to input the hash value into the dummy argument
@wraps(func)
def ttl_cached_func(*args, **kwargs):
hash = get_ttl_hash(ttl_seconds)
return cached_dummy_func(*args, ttl_dummy=hash, **kwargs)
return ttl_cached_func
return ttl_cache_deco
How to Use
To equip a function with TTL caching, simply prepend its definition with @ttl_cache(ttl_seconds=...)
.
@ttl_cache(ttl_seconds=3600)
def get_content(url):
...
Manually Toggling Cache On and Off
With our TTL cache decorator in place, there might be instances where you’d prefer fetching data directly rather than using the cache. To cater to this, it would be handy to have a “manual cache toggle” feature.
In the next article, we’ll discuss how to integrate this functionality.