Pythonで有効期限(TTL)付きキャッシュを実装する (2)
概要
今回も引き続き、有効期限(TTL)付きキャッシュの実装について解説します。
初めに前回の内容を簡単に振り返りましょう。
前回のまとめ
lru_cache
を使用すると関数にキャッシュ機能を持たせることができました。- 指定間隔で返り値が更新される
get_ttl_hash
関数を実装しました。 - 以下の方法で関数に有効期限付きキャッシュ機能をもたせることができました。
lru_cache
でキャッシュ機能を追加- ダミー引数を追加
- ダミー引数に
get_ttl_hash
の返り値を入力
「この処理を自動で行ってくれるデコレータを実装しよう」というのが今回の目標です。
有効期限(TTL)付きキャッシュデコレータを実装する
それでは、関数にTTLキャッシュ機能を追加するデコレータを実装していきます。
デコレータとは
まず、デコレータについて説明します。
デコレータとは簡単に言うと、関数を入力として関数を返す関数のことです。
以下の2つは同じ動作をします。
-
lru_cache
デコレータを使用:from functools import lru_cache @lru_cache def fibonacci(n): ... print(fibonacci(10))
-
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
は以下のような関数となります。
- 有効期限(
ttl_seconds
)を入力すると、次の機能を持つデコレータを返す。 - 関数を入力すると、TTLキャッシュ機能を追加した関数を返す。
つまり、デコレータを返す関数を実装する、ということになります。
デコレータを返す関数
有効期限 ttl_seconds
を引数として、デコレータを返す関数は以下ようになります。
def ttl_cache(ttl_seconds=3600):
def ttl_cache_deco(func):
...
return ttl_cache_deco
次に内側の ttl_cache_deco
デコレータを作成します。
TTLキャッシュ機能を追加するデコレータ
次に「関数を入力すると、TTLキャッシュ機能を追加した関数を返す」デコレータを作成します。
これは次の手順で作成できます。
- キャッシュ機能を追加し、ダミー引数を追加した関数を作成する
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キャッシュ機能を関数に追加することができます。
@ttl_cache(ttl_seconds=3600)
def get_content(url):
...
キャッシュのオンオフを手動で切り替えたい
今回、TTLキャッシュ機能をもつデコレータが完成しました。
しかし使用していく中で、「今はキャッシュを使わずにデータを直接取得したい」という場面が出てきました。
これを実現するため、次回は「キャッシュのオンオフ切り替え機能」の実装方法について紹介します。