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

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

今回も引き続き、有効期限(TTL)付きキャッシュの実装について解説します。
初めに前回の内容を簡単に振り返りましょう。

  1. lru_cache を使用すると関数にキャッシュ機能を持たせることができました。
  2. 指定間隔で返り値が更新される get_ttl_hash 関数を実装しました。
  3. 以下の方法で関数に有効期限付きキャッシュ機能をもたせることができました。
    1. lru_cache でキャッシュ機能を追加
    2. ダミー引数を追加
    3. ダミー引数に get_ttl_hash の返り値を入力

「この処理を自動で行ってくれるデコレータを実装しよう」というのが今回の目標です。

それでは、関数にTTLキャッシュ機能を追加するデコレータを実装していきます。

まず、デコレータについて説明します。
デコレータとは簡単に言うと、関数を入力として関数を返す関数のことです。

Example

以下の2つは同じ動作をします。

  1. lru_cache デコレータを使用:

    from functools import lru_cache
    
    
    @lru_cache
    def fibonacci(n):
        ...
    
    print(fibonacci(10))
    
  2. lru_cache を関数として直接呼び出して使用:

    from functools import lru_cache
    
    
    def fibonacci(n):
        ...
    
    print(lru_cache(fibonacci)(10))
    

やりたいことを整理しましょう。
まずは完成イメージから。

完成イメージ

関数の頭に@ttl_cache(ttl_seconds=60)と追加すると、
60秒間の有効期限付きキャッシュ機能がその関数に追加される。

@ttl_cache(ttl_seconds=60)
def myfunc(a, b):
    ...
  • 一度実行されると、計算結果がキャッシュとして記憶される。
  • キャッシュの有効期限(上の場合60秒)が切れると、
    次回実行時に値が再計算されキャッシュが更新される。

つまり、ttl_cacheは以下のような関数となります。

  1. 有効期限(ttl_seconds)を入力すると、次の機能を持つデコレータを返す。
  2. 関数を入力すると、TTLキャッシュ機能を追加した関数を返す。

つまり、デコレータを返す関数を実装する、ということになります。

有効期限 ttl_seconds を引数として、デコレータを返す関数は以下ようになります。

def ttl_cache(ttl_seconds=3600): 
    def ttl_cache_deco(func):
        ...
    return ttl_cache_deco

次に内側の ttl_cache_deco デコレータを作成します。

次に「関数を入力すると、TTLキャッシュ機能を追加した関数を返す」デコレータを作成します。
これは次の手順で作成できます。

  1. キャッシュ機能を追加し、ダミー引数を追加した関数を作成する
  2. get_ttl_hash(ttl_seconds) の値を計算し、ダミー引数に入力する関数を作成する。

このデコレータは以下のようなコードとなります。

def ttl_cache_deco(func):
    """関数を入力すると、有効期限(TTL)付きキャッシュ機能を実装した関数を返す."""
    # キャッシュ機能とダミー引数を追加した関数
    @lru_cache(maxsize=None)
    def cached_dummy_func(*args, ttl_dummy, **kwargs):
        del ttl_dummy  # ダミー引数を削除
        return func(*args, **kwargs)

    # 上記関数のダミー引数にハッシュ値を入力する関数
    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
関数の情報を失わないために

関数に上記デコレータを付与すると、コードを見てわかるように、
元の関数は内側のttl_cached_funcに変換されます。

その際に、そのままでは元の関数の情報(引数やdocstring等)が失われてします。
これを防ぐため、デコレータによって返される関数にfunctools.wrapsデコレータを付与します。
(参照: functools.wraps)

from functools import lru_cache, wraps
...
def ttl_cache_deco(func):
    ...
    @wraps(func)
    def ttl_cached_func(*args, **kwargs):
        ...

このように@wrapデコレータを付与することで、元の情報を失わずに関数の変換を行うことが出来ます。

これで有効期限付きキャッシュ機能を追加するデコレータが完成しました。
完成した ttl_cache デコレータの実装をまとめておきます。

import datetime
from functools import lru_cache, wraps


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


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

    Args:
        ttl_seconds (int, optional): 有効期限(秒). Defaults to 3600.
    """
    def ttl_cache_deco(func):
        """関数を入力すると、有効期限(TTL)付きキャッシュ機能を実装した関数を返す."""
        # キャッシュ機能とダミー引数を追加した関数
        @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, **kwargs):
            hash = get_ttl_hash(ttl_seconds)
            return cached_dummy_func(*args, ttl_dummy=hash, **kwargs)
    
        return ttl_cached_func
    return ttl_cache_deco

関数の定義の先頭に @ttl_cache(ttl_seconds=...) を付けることで、
TTLキャッシュ機能を関数に追加することができます。

Example
@ttl_cache(ttl_seconds=3600)
def get_content(url):
    ...

今回、TTLキャッシュ機能をもつデコレータが完成しました。
しかし使用していく中で、「今はキャッシュを使わずにデータを直接取得したい」という場面が出てきました。

これを実現するため、次回は「キャッシュのオンオフ切り替え機能」の実装方法について紹介します。

関連記事