Implementing TTL Cache in Python (1)

Series - Implementing TTL Cache in Python

In this article, we’ll discuss how to implement a caching mechanism in Python that has a Time-To-Live (TTL).
This cache can be useful in scenarios like:

  • Functions that connect to the internet to fetch data, but the data remains constant over a period.
    (e.g., weather forecast data)
  • Functions that are executed frequently but don’t require real-time data.

First, let’s see how to add caching to a Python function.

This can be achieved using the lru_cache decorator from functools (official documentation).

Info
For Python 3.9 and later, the cache decorator behaves the same as lru_cache(maxsize=None).
Example
  • A function to find Fibonacci numbers can be implemented as below, but without caching, it’s slow:

    def fibonacci(n):
        """Returns the nth Fibonacci number."""
        if n == 0:
            return 0
        elif n == 1:
            return 1
        else:
            return fibonacci(n - 2) + fibonacci(n - 1)
    
    • Example:

      %time fibonacci(40)
      
      # CPU times: user 22.1 s, sys: 0 ns, total: 22.1 s
      # Wall time: 22.1 s
      # 102334155
      
  • By using the lru_cache decorator to cache the function’s output,
    the computation becomes much faster.
    (This process is often referred to as “memoization”.)

    from functools import lru_cache
    
    @lru_cache(maxsize=None)
    def fibonacci(n):
        """Returns the nth Fibonacci number."""
        if n == 0:
            return 0
        elif n == 1:
            return 1
        else:
            return fibonacci(n - 2) + fibonacci(n - 1)
    
    • Example:

      %time fibonacci(40)
      
      # CPU times: user 52 µs, sys: 1 µs, total: 53 µs
      # Wall time: 58.9 µs
      # 102334155
      

So, how can we implement caching with TTL?
We can archive this by combining:

  • A function that changes its return value at specified intervals.
  • A dummy argument.

We’ll explain with the example below.
(This idea is based on an answer from StackOverflow).

Example
  1. Consider a function that fetches content from a specified URL:

    @lru_cache(maxsize=None)
    def get_content(url):
        """Fetch content from a URL."""
        ...
    
  2. Add a new dummy argument dummy to this function:

    @lru_cache(maxsize=None)
    def get_content(url, dummy):
        """Fetch content from a URL."""
        ...
    
  3. Feed the return value of a function get_ttl_hash that changes its return value at specified intervals, to this dummy parameter:

    hash = get_ttl_hash()
    get_content(url, dummy=hash)
    
  4. With this approach, the function arguments remain the same for the duration of the TTL, utilizing the cache. Once the TTL expires, the argument changes, and the cache is bypassed, forcing a fresh computation.

A function that changes its return value at specified intervals can be implemented as follows:

import datetime

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))

Now we have a function with TTL caching capabilities.

However, continually setting up a dummy argument and assigning get_ttl_hash to it can be tedious. So, in the next part, we’ll implement a decorator that does this automatically for us.

Related Content