Pythonで有効期限(TTL)付きキャッシュを実装する (1)

シリーズ - Pythonで有効期限(TTL)付きキャッシュを実装する

今回は、 Python で有効期限(Time-To-Live, TTL)を持つキャッシュ機構を実装する方法を紹介します。
このキャッシュ機構は、以下のような用途で役に立ちます。

  • インターネットに接続してデータを取得する関数であるが、そのデータが一定期間変化しない場合。
    (天気予報のデータなど)
  • 頻繁に実行する関数であるが、リアルタイムのデータを必要としない場合。

まず初めに、Python の関数にキャッシュ機能を付ける方法を紹介します。

これは、functools に含まれる lru_cacheデコレータを使用して実現できます(公式ドキュメント)。

Info
Python 3.9 以降の場合、cache デコレータが lru_cache(maxsize=None) と同じ動作をします。
Example
  • フィボナッチ数を求める関数は以下のように実装できますが、このままではとても遅いです。

    def fibonacci(n):
        """n番目のフィボナッチ数を出力する."""
        if n == 0:
            return 0
        elif n == 1:
            return 1
        else:
            return fibonacci(n - 2) + fibonacci(n - 1)
    
    • %time fibonacci(40)
      
      # CPU times: user 22.1 s, sys: 0 ns, total: 22.1 s
      # Wall time: 22.1 s
      # 102334155
      
  • lru_cache デコレータを用いて関数の出力をキャッシュすることで、計算が高速になります。
    (このことを「メモ化する」とも呼ばれます。)

    from functools import lru_cache
    
    
    @lru_cache(maxsize=None)
    def fibonacci(n):
        """n番目のフィボナッチ数を出力する."""
        if n == 0:
            return 0
        elif n == 1:
            return 1
        else:
            return fibonacci(n - 2) + fibonacci(n - 1)
    
    • %time fibonacci(40)
      
      # CPU times: user 52 µs, sys: 1 µs, total: 53 µs
      # Wall time: 58.9 µs
      # 102334155
      

それでは、有効期限付きのキャッシュを実現するにはどうすればよいでしょうか?
それは、指定した間隔で返り値が変化する関数とダミー引数の組み合わせで実現できます。
以下で具体例を用いて説明します。
(このアイディアは stackoverflowの回答 によるものです)

Example
  1. 以下のような指定されたURLのコンテンツを取得する関数があるとします。

    @lru_cache(maxsize=None)
    def get_content(url):
        """URLのコンテンツを取得する."""
        ...
    
  2. この関数に新たなダミー引数 dummy を追加します。

    @lru_cache(maxsize=None)
    def get_content(url, dummy):
        """URLのコンテンツを取得する."""
        ...
    
  3. このダミー引数に、指定した間隔で値が変化する関数 get_ttl_hash の返り値を入力します。

    hash = get_ttl_hash()
    get_content(url, dummy=hash)
    
  4. こうすることで、一定期間中は関数の引数が同じ値になるのでキャッシュが使用され、
    期限を過ぎると引数の値が変わるためキャッシュが使用されず、
    再度関数の返り値が計算されるようになります。

指定した間隔で返り値が変化する関数は次のように実装できます。

import datetime


def get_ttl_hash(seconds=3600):
    """TTLキャッシュ用のハッシュ値を計算する。

    Args:
        seconds (int, optional): 有効期限(秒). Defaults to 3600.

    Returns:
        int: ハッシュ値
    """
    utime = datetime.datetime.now().timestamp()
    return round(utime / (seconds + 1))

これでTTLキャッシュ機能を持つ関数を作ることができました。

ただ、毎回ダミー引数を用意してそこに get_ttl_hash を代入するのは面倒なので、
次回は自動でそれを実現してくれるデコレータを実装します。

関連記事