在某些情況下,我們需要控制物件的建立次數或快取計算結果以提升效能。本章介紹 Hook 系統中使用的快取模式。

模組級快取

Python 模組是天然的單例——模組只會被載入一次。利用這個特性,可以實作簡單的快取。

實際範例:配置快取

來自 .claude/lib/config_loader.py

 1from typing import Optional
 2
 3# 模組級快取變數
 4_agents_config_cache: Optional[dict] = None
 5_quality_rules_cache: Optional[dict] = None
 6
 7def load_agents_config() -> dict:
 8    """
 9    載入代理人配置
10
11    使用模組級快取,避免重複讀取檔案。
12    """
13    global _agents_config_cache
14
15    # 檢查快取
16    if _agents_config_cache is None:
17        try:
18            _agents_config_cache = load_config("agents")
19        except FileNotFoundError:
20            _agents_config_cache = _get_default_agents_config()
21
22    return _agents_config_cache
23
24def load_quality_rules() -> dict:
25    """載入品質規則配置"""
26    global _quality_rules_cache
27
28    if _quality_rules_cache is None:
29        try:
30            _quality_rules_cache = load_config("quality_rules")
31        except FileNotFoundError:
32            _quality_rules_cache = _get_default_quality_rules()
33
34    return _quality_rules_cache
35
36def clear_config_cache() -> None:
37    """
38    清除配置快取
39
40    用於測試或配置熱更新。
41    """
42    global _agents_config_cache, _quality_rules_cache
43    _agents_config_cache = None
44    _quality_rules_cache = None

使用方式

 1# 第一次呼叫:從檔案載入
 2config1 = load_agents_config()
 3
 4# 第二次呼叫:直接返回快取
 5config2 = load_agents_config()
 6
 7# config1 is config2  # True
 8
 9# 需要重新載入時
10clear_config_cache()
11config3 = load_agents_config()  # 重新從檔案載入

為什麼使用這個模式?

效能考量

 1# 沒有快取:每次都讀取檔案
 2def load_config_slow() -> dict:
 3    with open("config.yaml") as f:
 4        return yaml.safe_load(f)  # I/O 操作
 5
 6# 有快取:只讀取一次
 7def load_config_fast() -> dict:
 8    global _cache
 9    if _cache is None:
10        with open("config.yaml") as f:
11            _cache = yaml.safe_load(f)
12    return _cache

一致性

1# 確保所有地方使用相同的配置
2config_a = load_agents_config()
3config_b = load_agents_config()
4
5# 修改 config_a 會影響 config_b(因為是同一個物件)
6# 這可能是優點也可能是缺點,取決於使用場景

函式裝飾器快取

@functools.lru_cache

Python 標準庫提供的快取裝飾器:

 1from functools import lru_cache
 2
 3@lru_cache(maxsize=128)
 4def expensive_computation(n: int) -> int:
 5    """計算結果會被快取"""
 6    print(f"Computing for {n}...")
 7    return sum(range(n))
 8
 9# 第一次呼叫:執行計算
10result1 = expensive_computation(1000)  # 印出 "Computing for 1000..."
11
12# 第二次呼叫:直接返回快取
13result2 = expensive_computation(1000)  # 不印出任何東西
14
15# 清除快取
16expensive_computation.cache_clear()

@functools.cache (Python 3.9+)

無大小限制的快取:

 1from functools import cache
 2
 3@cache
 4def fibonacci(n: int) -> int:
 5    if n < 2:
 6        return n
 7    return fibonacci(n - 1) + fibonacci(n - 2)
 8
 9# 快取讓遞迴變得高效
10fibonacci(100)  # 瞬間完成

手動實作快取

字典快取

 1_cache: dict = {}
 2
 3def get_user(user_id: int) -> dict:
 4    """取得使用者資料,使用快取"""
 5    if user_id not in _cache:
 6        _cache[user_id] = fetch_from_database(user_id)
 7    return _cache[user_id]
 8
 9def invalidate_user(user_id: int) -> None:
10    """使特定使用者的快取失效"""
11    _cache.pop(user_id, None)
12
13def clear_all_cache() -> None:
14    """清除所有快取"""
15    _cache.clear()

帶過期時間的快取

 1from time import time
 2from typing import Optional, Any
 3
 4_cache: dict = {}
 5_cache_time: dict = {}
 6CACHE_TTL = 300  # 5 分鐘
 7
 8def get_with_ttl(key: str) -> Optional[Any]:
 9    """取得快取,檢查是否過期"""
10    if key in _cache:
11        if time() - _cache_time[key] < CACHE_TTL:
12            return _cache[key]
13        else:
14            # 快取過期
15            del _cache[key]
16            del _cache_time[key]
17    return None
18
19def set_with_ttl(key: str, value: Any) -> None:
20    """設定快取"""
21    _cache[key] = value
22    _cache_time[key] = time()

單例模式

當確實需要單例時的實作方式:

使用模組(最簡單)

 1# singleton.py
 2class _Singleton:
 3    def __init__(self):
 4        self.value = 0
 5
 6instance = _Singleton()
 7
 8# 使用
 9from singleton import instance
10instance.value = 42

使用類別裝飾器

 1def singleton(cls):
 2    instances = {}
 3
 4    def get_instance(*args, **kwargs):
 5        if cls not in instances:
 6            instances[cls] = cls(*args, **kwargs)
 7        return instances[cls]
 8
 9    return get_instance
10
11@singleton
12class Database:
13    def __init__(self):
14        print("Connecting to database...")
15
16# 使用
17db1 = Database()  # 印出 "Connecting..."
18db2 = Database()  # 不印出(返回同一個實例)
19db1 is db2  # True

使用 new

 1class Singleton:
 2    _instance = None
 3
 4    def __new__(cls):
 5        if cls._instance is None:
 6            cls._instance = super().__new__(cls)
 7        return cls._instance
 8
 9# 使用
10s1 = Singleton()
11s2 = Singleton()
12s1 is s2  # True

Hook 系統的實際應用

配置載入器的設計

 1# config_loader.py
 2
 3from typing import Optional
 4
 5# 私有快取
 6_agents_config_cache: Optional[dict] = None
 7
 8def load_agents_config() -> dict:
 9    """
10    載入代理人配置
11
12    特點:
13    1. 使用模組級快取
14    2. 支援預設配置
15    3. 提供清除快取的方法
16    """
17    global _agents_config_cache
18
19    if _agents_config_cache is None:
20        try:
21            _agents_config_cache = load_config("agents")
22        except FileNotFoundError:
23            # 返回預設配置
24            _agents_config_cache = _get_default_agents_config()
25
26    return _agents_config_cache
27
28def _get_default_agents_config() -> dict:
29    """預設配置"""
30    return {
31        "known_agents": [
32            "basil-hook-architect",
33            "thyme-documentation-integrator",
34            # ...
35        ],
36        "agent_dispatch_rules": {
37            "Hook 開發": "basil-hook-architect",
38            # ...
39        }
40    }

測試快取程式碼

 1import unittest
 2
 3class TestConfigLoader(unittest.TestCase):
 4
 5    def setUp(self):
 6        # 每個測試前清除快取
 7        clear_config_cache()
 8
 9    def tearDown(self):
10        # 每個測試後清除快取
11        clear_config_cache()
12
13    def test_config_is_cached(self):
14        """測試配置被快取"""
15        config1 = load_agents_config()
16        config2 = load_agents_config()
17
18        # 應該是同一個物件
19        self.assertIs(config1, config2)
20
21    def test_clear_cache_works(self):
22        """測試清除快取"""
23        config1 = load_agents_config()
24        clear_config_cache()
25        config2 = load_agents_config()
26
27        # 應該是不同的物件
28        self.assertIsNot(config1, config2)

最佳實踐

1. 提供清除快取的方法

1# 好:可以清除快取
2def clear_config_cache():
3    global _cache
4    _cache = None
5
6# 不好:無法重新載入

2. 考慮執行緒安全

 1import threading
 2
 3_lock = threading.Lock()
 4_cache = None
 5
 6def get_cached_value():
 7    global _cache
 8    if _cache is None:
 9        with _lock:
10            # 雙重檢查
11            if _cache is None:
12                _cache = expensive_computation()
13    return _cache

3. 文件化快取行為

1def load_config() -> dict:
2    """
3    載入配置
4
5    Note:
6        結果會被快取,後續呼叫返回同一個物件。
7        使用 clear_config_cache() 可重新載入。
8    """

思考題

  1. 模組級快取和 @lru_cache 有什麼區別?
  2. 為什麼 clear_config_cache() 很重要?
  3. 在多執行緒環境下,模組級快取可能有什麼問題?

實作練習

  1. 使用 @lru_cache 實作一個帶快取的 API 呼叫函式
  2. 實作一個帶 TTL(存活時間)的快取
  3. 為現有的快取添加執行緒安全保護

延伸閱讀(進階系列)


上一章:工廠模式 下一模組:錯誤處理與測試