Cached

キャッシュライブラリPythonデコレータメモ化メモリキャッシュfunctools

キャッシュライブラリ

Cached

概要

CachedはPythonの標準ライブラリであるfunctoolsモジュールに含まれるメモ化機能で、関数の結果をキャッシュして計算の重複を避けることができます。

詳細

Cachedは、Pythonの標準ライブラリfunctoolsモジュールが提供するlru_cache(およびPython 3.9以降のcache)デコレータを指し、関数の呼び出し結果をメモリに保存してメモ化を実現します。lru_cacheはLRU(Least Recently Used)アルゴリズムを使用してキャッシュサイズを管理し、最近使用されていないアイテムを自動的に削除します。デコレータ構文により関数に簡単に適用でき、フィボナッチ数列の計算や重い計算処理の高速化に威力を発揮します。hasharbleな引数のみをサポートし、辞書やリストなどのmutableオブジェクトは直接キャッシュできません。cache_info()メソッドでヒット/ミス統計を確認でき、cache_clear()でキャッシュをクリアできます。標準ライブラリのため追加のインストールが不要で、軽量かつ信頼性が高く、多くのPythonアプリケーションで活用されています。Python 3.9以降では無制限キャッシュのcacheデコレータも利用可能です。

メリット・デメリット

メリット

  • 標準ライブラリ: 追加インストール不要で即座に利用可能
  • シンプルAPI: デコレータ構文で簡単に適用
  • 高性能: C実装による高速な動作
  • 統計機能: ヒット/ミス率を簡単に確認可能
  • メモリ管理: LRUアルゴリズムによる自動的なメモリ管理
  • 信頼性: Python標準ライブラリとして十分にテスト済み
  • 軽量: オーバーヘッドが最小限

デメリット

  • ハッシュ可能型のみ: 辞書やリストなど一部のオブジェクトは直接キャッシュ不可
  • メモリキャッシュのみ: 永続化やプロセス間共有は不可
  • メソッドキャッシュの問題: インスタンスメソッドのキャッシュでガベージコレクションが阻害される場合がある
  • 単一プロセス: 分散キャッシュやマルチプロセス対応なし
  • TTL機能なし: 時間ベースの有効期限設定は非対応
  • カスタマイズ制限: 高度なキャッシュポリシーの設定は困難

主要リンク

書き方の例

基本的なLRUキャッシュ

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    """フィボナッチ数列の計算をキャッシュ"""
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# 使用例
print(fibonacci(100))  # 初回は計算、2回目以降はキャッシュから返却
print(fibonacci.cache_info())  # CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)

無制限キャッシュ(Python 3.9+)

from functools import cache

@cache
def expensive_calculation(x, y):
    """重い計算処理"""
    print(f"計算中: {x} + {y}")
    time.sleep(1)  # 重い処理をシミュレート
    return x + y

# 使用例
print(expensive_calculation(1, 2))  # 計算中: 1 + 2
print(expensive_calculation(1, 2))  # キャッシュから即座に返却

キャッシュ統計の活用

@lru_cache(maxsize=64)
def get_user_data(user_id):
    """ユーザーデータの取得(データベースアクセスをシミュレート)"""
    print(f"データベースからユーザー {user_id} を取得")
    return {"id": user_id, "name": f"User {user_id}"}

# 複数回アクセス
for user_id in [1, 2, 1, 3, 2, 1]:
    user = get_user_data(user_id)
    print(f"取得: {user}")

# キャッシュ統計を確認
info = get_user_data.cache_info()
print(f"ヒット率: {info.hits / (info.hits + info.misses):.2%}")
print(f"キャッシュサイズ: {info.currsize}/{info.maxsize}")

型ヒント付きキャッシュ

from functools import lru_cache
from typing import Dict, Any

@lru_cache(maxsize=256)
def fetch_config(environment: str) -> Dict[str, Any]:
    """環境設定の取得"""
    config_map = {
        "development": {"debug": True, "db_host": "localhost"},
        "production": {"debug": False, "db_host": "prod.example.com"},
        "testing": {"debug": True, "db_host": "test.example.com"}
    }
    return config_map.get(environment, {})

# 使用例
dev_config = fetch_config("development")
prod_config = fetch_config("production")

カスタムキャッシュ実装

def simple_cache(func):
    """シンプルなキャッシュデコレータの実装例"""
    cache = {}
    
    def wrapper(*args, **kwargs):
        # キーを生成(引数をハッシュ化)
        key = str(args) + str(sorted(kwargs.items()))
        
        if key in cache:
            return cache[key]
        
        # 結果を計算してキャッシュに保存
        result = func(*args, **kwargs)
        cache[key] = result
        return result
    
    # キャッシュクリア機能を追加
    wrapper.cache_clear = lambda: cache.clear()
    wrapper.cache_size = lambda: len(cache)
    
    return wrapper

@simple_cache
def multiply(a, b):
    print(f"計算: {a} * {b}")
    return a * b

# 使用例
print(multiply(3, 4))  # 計算: 3 * 4
print(multiply(3, 4))  # キャッシュから返却
print(f"キャッシュサイズ: {multiply.cache_size()}")

条件付きキャッシュ

from functools import lru_cache
import time

def conditional_cache(func):
    """条件に応じてキャッシュを使い分ける"""
    cached_func = lru_cache(maxsize=128)(func)
    
    def wrapper(*args, **kwargs):
        # 特定の条件でキャッシュを無効化
        if kwargs.get('force_refresh', False):
            # キャッシュをバイパスして直接実行
            kwargs.pop('force_refresh', None)
            return func(*args, **kwargs)
        return cached_func(*args, **kwargs)
    
    wrapper.cache_info = cached_func.cache_info
    wrapper.cache_clear = cached_func.cache_clear
    return wrapper

@conditional_cache
def get_current_time():
    return time.time()

# 通常はキャッシュされる
print(get_current_time())
print(get_current_time())  # 同じ値

# 強制リフレッシュ
print(get_current_time(force_refresh=True))  # 新しい値