4.4 單例與快取模式
4.4 單例與快取模式
在某些情況下,我們需要控制物件的建立次數或快取計算結果以提升效能。本章介紹 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 # TrueHook 系統的實際應用
配置載入器的設計
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 _cache3. 文件化快取行為
1def load_config() -> dict:
2 """
3 載入配置
4
5 Note:
6 結果會被快取,後續呼叫返回同一個物件。
7 使用 clear_config_cache() 可重新載入。
8 """思考題
- 模組級快取和
@lru_cache有什麼區別? - 為什麼
clear_config_cache()很重要? - 在多執行緒環境下,模組級快取可能有什麼問題?
實作練習
- 使用
@lru_cache實作一個帶快取的 API 呼叫函式 - 實作一個帶 TTL(存活時間)的快取
- 為現有的快取添加執行緒安全保護
延伸閱讀(進階系列)
- 進階設計模式 - 更多設計模式的深入探討
- 快取生命週期管理 - 進階快取策略
- 實戰效能優化:LRU 快取 - lru_cache 的實際應用