はじめに

Pythonではfunctools.lru_cacheをはじめとした関数ベースのキャッシュ機構(メモ化)が幅広く利用されている。
これによりパフォーマンスの改善が見込める一方、副作用や状態不整合といった予期せぬバグの温床になるケースも少なくない。

レビューアーは、単にキャッシュが使われていることに気づくだけでは不十分であり、その適用箇所・副作用・破棄戦略まで含めて設計の妥当性を評価する必要がある。

functools.lru_cacheの基本構造と誤用パターン

利用例:純粋関数に対するキャッシュ

純粋関数のキャッシュ
from functools import lru_cache

@lru_cache(maxsize=128)
def factorial(n):
    return 1 if n == 0 else n * factorial(n - 1)

このような引数と戻り値の関係が安定している関数に対するキャッシュは非常に有効で、レビュー指摘の必要性は低い。

しかし、実際の業務ロジックでは以下のような副作用や状態依存を持つ関数がキャッシュされてしまっているケースが散見される。

状態依存のキャッシュ
@lru_cache()
def get_config():
    with open("config.yaml") as f:
        return yaml.safe_load(f)
Comment
@Reviewer: この関数はファイルI/Oの副作用を含むため、永続キャッシュの対象とするのは危険です。変更検知や明示的な無効化処理が必要です。

レビュー観点1:副作用を持つ関数のキャッシュは本当に安全か?

ファイル読み込み・データベースクエリ・外部API呼び出しなどの副作用処理に対し、lru_cacheやメモ化が適用されている場合、極めて慎重な判断が必要である。

APIレスポンスのキャッシュ
@lru_cache()
def get_user_profile(user_id):
    return requests.get(f"https://api.example.com/users/{user_id}").json()
  • このAPIは定期的に更新されるか?
  • 認証やセッションによって結果が変化するか?
  • キャッシュ破棄はどこで担保されているか?

といった環境依存性の評価が欠かせない。

Comment
@Reviewer: レスポンスの内容が外部の更新に依存する場合、キャッシュ有効期限やバージョン判定を組み合わせないと整合性が崩れる可能性があります。

レビュー観点2:可変引数やmutable型のキャッシュ誤動作

lru_cacheの対象関数にリスト・辞書・セットなどのmutable型が引数として使われている場合、TypeErrorまたはキャッシュバグが発生する。

可変型引数での失敗
@lru_cache()
def sum_list(numbers: list):
    return sum(numbers)

sum_list([1, 2, 3])
Comment
@Reviewer: `list`型はハッシュ化できないため、このコードは実行時にTypeErrorになります。キャッシュ対象として適切な引数型か再評価が必要です。

対策としては、引数をtupleに変換する、または引数に対してfrozensetなどのイミュータブル型を用いることが求められる。

レビュー観点3:キャッシュの破棄戦略は明確か?

一度キャッシュされた値がいつ・どこで・どの条件で無効化されるのかが不明なまま運用されているケースは非常に多い。

キャッシュを無効化しない例
@lru_cache()
def get_db_schema():
    return query_schema_via_sql()

このようなコードに対し、スキーマが変更された際のキャッシュ破棄手段がなければ、構造不整合によるシステムエラーを引き起こす可能性がある。

Comment
@Reviewer: スキーマ取得結果をキャッシュする際は、デプロイや環境変更によって強制的にクリアされる設計が必要です。

手動破棄のためのcache_clear()の呼び出し設計が適切に組み込まれているかを確認する。

キャッシュクリアの例
get_db_schema.cache_clear()

レビュー観点4:メモ化のスコープと永続性を見極める

lru_cacheとは別に、関数内で辞書や変数を用いて自作メモ化が行われているケースもある。

自作メモ化の例
_cache = {}

def compute(x):
    if x in _cache:
        return _cache[x]
    result = complex_calc(x)
    _cache[x] = result
    return result

このような構造では以下の観点でレビューが必要となる。

  • グローバルスコープで他関数と共有されていないか?
  • 初期化・クリアタイミングが明示されているか?
  • 同期処理(thread/process)での一貫性は保たれているか?
Comment
@Reviewer: `_cache`が他ファイルでも共有されている場合、状態の競合や破損リスクがあります。クラススコープやインスタンス変数への隔離を検討してください。

キャッシュ設計のレビューにおける補助質問

キャッシュがコードに登場した際、レビューアーは以下のような補助的質問を投げかけることで設計の不整合を見抜ける可能性が高まる。

  • このデータはどれくらいの頻度で変わるものか?
  • 誰が・いつ・どこでキャッシュをクリアする責任を持つのか?
  • キャッシュされた値が永続的であると誤解されない設計になっているか?
  • 本当にパフォーマンスがボトルネックになっているのか?(計測済みか?)

キャッシュ設計レビューの観点リスト

チェックポイントまとめ
  • 副作用を持つ関数にキャッシュが適用されていないか?
  • 引数がイミュータブルかつハッシュ可能であることが確認されているか?
  • 明示的なキャッシュ破棄手段(cache_clear等)が存在するか?
  • 外部状態(ファイル・DB・API)との整合性を保てているか?
  • テストコードにおいてキャッシュによる結果汚染が発生していないか?

おわりに

キャッシュは単に「速くなる魔法」ではなく、状態の複製と管理の責任を導入する構造的選択である。
レビューアーは、キャッシュを見たら喜ぶのではなく、「そこにバグの温床はないか?」と疑う視点を持つべきである。

副作用のない純粋関数ならば問題は起きにくいが、業務アプリケーションでは外部依存や状態遷移が不可避である。
その中で、整合性・破棄設計・可視性・引数妥当性といった複数軸での構造的レビューが求められる。