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

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

今回は、前回までに実装したTTLキャッシュデコレータに対し、
キャッシュのオン・オフを切り替える機能を追加します。

今回追加するキャッシュ切り替え機能の仕様は以下のとおりです:

  1. lru_cacheデコレータが付けられた関数にはuse_cache引数が追加される。
    この引数でキャッシュのオンオフを切り替えられるようになる。
  2. use_cache=False と指定するとキャッシュを無視して実行される。
    計算結果はキャッシュに保存される。
  3. 再度 use_cache=True とすると、キャッシュの内容が利用される。

実装方法は色々と考えられそうですが、今回は以下のように行いました。

  • デコレータの入力関数に cache_offset というカウンターを属性として追加し、
    use_cache=False で関数が実行された際にカウントアップさせ、ハッシュ値に加算する。

以下コードのハイライト部分が、今回の変更箇所です。

def ttl_cache(ttl_seconds=3600, use_cache=True):
    """有効期限(TTL)付きキャッシュ機能を持つデコレータ.

    関数に use_cache 引数を追加し、キャッシュのオン・オフを切り替えることができる.

    Args:
        ttl_seconds (int or None, optional): 有効期限(秒). Defaults to 3600.
        use_cache (bool, optional): デフォルトでキャッシュを有効化するかどうか. Defaults to True.
    """
    def ttl_cache_deco(func):
        """関数を入力として、それに有効期限付きキャッシュ機能を実装した関数を返す。"""
        # キャッシュ機能とダミー引数を追加した関数を作成
        @lru_cache(maxsize=None)
        def cached_dummy_func(*args, ttl_dummy, **kwargs):
            del ttl_dummy
            return func(*args, **kwargs)
 
        # 自動でハッシュ値を計算してダミー引数に入力する関数を作成
        @wraps(func)
        def ttl_cached_func(*args, use_cache=use_cache, **kwargs):
            if not use_cache:
                ttl_cached_func.cache_offset += 1
            hash = get_ttl_hash(ttl_seconds) + ttl_cached_func.cache_offset
            return cached_dummy_func(*args, ttl_dummy=hash, **kwargs)
        
        ttl_cached_func.cache_offset = 0
    
        return ttl_cached_func
    return ttl_cache_deco
Example

以下、使用例です。
意図通りの動作が確認できました。

# In [1] --------------------
import random
from python_utils import ttl_cache


@ttl_cache(ttl_seconds=20)
def get_random_int():
    return random.randint(0, 100)

# In [2] --------------------
get_random_int()

# Out: 64

# In [3] --------------------
get_random_int()

# Out: 64

# In [4] --------------------
get_random_int(use_cache=False)

# Out: 36

# In [5] --------------------
get_random_int(use_cache=False)

# Out: 0

# In [6] --------------------
get_random_int(use_cache=False)

# Out: 91

# In [7] --------------------
get_random_int()

# Out: 91

# In [8] --------------------
get_random_int()

# Out: 91

現状ではVSCodeのようなコード補完機能のあるエディタにおいて、
ttl_cacheデコレータを使った関数でuse_cacheオプションのコード補完は動作しません。

少し強引ですが、以下のように関数のシグネチャに追加すると、コード補完で表示されるようになります。

import inspect
...
def ttl_cache(ttl_seconds=3600, use_cache=True):
    ...
    def ttl_cache_deco(func):
        ...
        ttl_cached_func.cache_offset = 0
        # 元の関数のシグネチャをコピーし、"use_cache"パラメータを追加
        sig = inspect.signature(func)
        params = list(sig.parameters.values())
        params.append(inspect.Parameter("use_cache", inspect.Parameter.KEYWORD_ONLY, default=use_cache))
        new_sig = sig.replace(parameters=params)
        ttl_cached_func.__signature__ = new_sig

        return ttl_cached_func
    return ttl_cache_deco

PythonでTTLキャッシュ機能を実装するシリーズは、ここで一旦終了です。
他にも以下のような改修案が考えられます。必要に応じてカスタマイズしてみてください。

改修案
  • ttl_secondsuse_cacheを指定しなくても使えるようにする。
  • maxsizeを変更できるようにする。
  • use_cache を関数の引数としてではなく属性として追加する。
Note
  • 今回はサードパーティ製ライブラリを使用せずに実装を行いましたが、
    cachetools というライブラリにもTTLキャッシュ機能を持つデコレータが存在します。

  • lru_cache の仕様上、キャッシュ機能を追加する関数の引数はハッシュ化可能である必要があります。
    リストを入力とする関数などは今回のキャッシュデコレータを直接使用することは出来ないため、
    入力をタプルにする等の調整が必要になります。

関連記事