本案例基於 .claude/lib/hook_validator.py 的實際程式碼,展示如何透過正則表達式預編譯來減少重複編譯的開銷。

先備知識

問題背景

現有設計

hook_validator.py 是一個 Hook 合規性驗證工具,用於檢查 Hook 腳本是否遵循專案規範。它定義了多組正則表達式模式來偵測各種程式碼特徵:

 1class HookValidator:
 2    """Hook 合規性驗證器"""
 3
 4    # 共用模組導入模式
 5    HOOK_IO_PATTERNS = [
 6        r"from\s+hook_io\s+import",
 7        r"from\s+lib\.hook_io\s+import",
 8    ]
 9
10    HOOK_LOGGING_PATTERNS = [
11        r"from\s+hook_logging\s+import",
12        r"from\s+lib\.hook_logging\s+import",
13    ]
14
15    CONFIG_LOADER_PATTERNS = [
16        r"from\s+config_loader\s+import",
17        r"from\s+lib\.config_loader\s+import",
18    ]
19
20    GIT_UTILS_PATTERNS = [
21        r"from\s+git_utils\s+import",
22        r"from\s+lib\.git_utils\s+import",
23    ]
24
25    # 輸出函式使用模式
26    OUTPUT_PATTERNS = [
27        r"write_hook_output\s*\(",
28        r"create_pretooluse_output\s*\(",
29        r"create_posttooluse_output\s*\(",
30    ]
31
32    # 不推薦的輸出模式
33    BAD_OUTPUT_PATTERNS = [
34        r'print\s*\(\s*json\.dumps\s*\(',
35        r'sys\.stdout\.write\s*\(\s*json\.dumps\s*\(',
36    ]
37
38    # 命名規範模式
39    VALID_NAME_PATTERNS = [
40        r"^[a-z0-9](/python-advanced/08-practical-optimization/case-studies/regex-precompile/[a-z0-9\-_]*[a-z0-9])?\.py$",
41    ]
42
43    # Hook 類型推測模式
44    HOOK_TYPE_HINTS = [
45        ("PreToolUse", r"create_pretooluse_output|permissionDecision"),
46        ("PostToolUse", r"create_posttooluse_output|additionalContext"),
47        ("Stop", r"Stop|subagent"),
48        ("SessionStart", r"SessionStart|session_id"),
49    ]

目前的實作將模式儲存為字串列表,並在輔助方法中使用 re.search() 進行匹配:

 1def _has_import(self, content: str, patterns: List[str]) -> bool:
 2    """檢查是否有符合任一模式的導入"""
 3    return any(
 4        re.search(pattern, content)
 5        for pattern in patterns
 6    )
 7
 8def _matches_pattern(self, content: str, patterns: List[str]) -> bool:
 9    """檢查是否符合任一模式"""
10    return any(
11        re.search(pattern, content)
12        for pattern in patterns
13    )

Python re 的內部快取

在討論優化之前,我們需要了解 Python re 模組的內部機制。

當你使用 re.search(pattern, string) 這樣的函式時,Python 會在內部執行兩個步驟:

  1. 編譯:將正則表達式字串轉換為內部的 pattern 物件
  2. 匹配:使用 pattern 物件對目標字串進行匹配

為了避免重複編譯,re 模組內建了一個 LRU 快取:

 1# Python 內部實作概念(簡化版)
 2_cache = {}
 3_MAXCACHE = 512  # Python 3.12 的預設值
 4
 5def _compile(pattern, flags=0):
 6    key = (type(pattern), pattern, flags)
 7    if key in _cache:
 8        return _cache[key]  # 快取命中
 9
10    # 實際編譯
11    compiled = sre_compile.compile(pattern, flags)
12
13    # 儲存到快取
14    if len(_cache) >= _MAXCACHE:
15        _cache.clear()  # 快取滿了就清空
16    _cache[key] = compiled
17
18    return compiled

你可以驗證這個快取的存在:

 1import re
 2
 3# 查看快取大小
 4print(f"快取大小上限: {re._MAXCACHE}")
 5
 6# 第一次呼叫會編譯
 7re.search(r'\d+', 'test123')
 8
 9# 查看快取內容(僅供觀察,不建議在生產環境使用)
10print(f"目前快取數量: {len(re._cache)}")

為什麼還需要手動預編譯?

既然 re 有內建快取,為什麼還需要手動使用 re.compile()?原因有幾個:

1. 快取查找有開銷

每次使用 re.search() 時,都需要:

 1# 虛擬碼:每次 re.search() 的內部流程
 2def search(pattern, string, flags=0):
 3    # 1. 建立快取鍵(需要計算 hash)
 4    key = (type(pattern), pattern, flags)
 5
 6    # 2. 查找快取(dict lookup)
 7    if key in _cache:
 8        compiled = _cache[key]
 9    else:
10        compiled = _compile_and_cache(pattern, flags)
11
12    # 3. 執行匹配
13    return compiled.search(string)

相比之下,預編譯後直接使用:

1# 預編譯
2pattern = re.compile(r'\d+')
3
4# 直接使用,無需快取查找
5pattern.search(string)

2. 快取可能被清空

當快取達到上限(預設 512 個模式)時,整個快取會被清空:

1if len(_cache) >= _MAXCACHE:
2    _cache.clear()  # 全部清空!

這表示在大型專案中,你的常用模式可能會被意外從快取中移除。

3. 語意更清晰

預編譯讓程式碼意圖更明確:

 1# 不清楚:pattern 是什麼時候編譯的?
 2def check(content):
 3    if re.search(r'pattern1', content):
 4        ...
 5    if re.search(r'pattern2', content):
 6        ...
 7
 8# 清楚:模式在類別載入時就編譯好了
 9class Validator:
10    PATTERN1 = re.compile(r'pattern1')
11    PATTERN2 = re.compile(r'pattern2')
12
13    def check(self, content):
14        if self.PATTERN1.search(content):
15            ...
16        if self.PATTERN2.search(content):
17            ...

進階解決方案

實作步驟

步驟 1:識別需要預編譯的模式

首先,找出所有會被重複使用的正則表達式。在 hook_validator.py 中,以下模式會在每次驗證時使用:

  • 導入檢查模式(7 組,共 14 個模式)
  • 輸出格式檢查模式(5 個模式)
  • 命名規範模式(1 個模式)
  • Hook 類型推測模式(4 個模式)

步驟 2:建立預編譯版本

將字串模式轉換為已編譯的 pattern 物件:

 1import re
 2from typing import Pattern, List, Tuple
 3
 4class HookValidatorOptimized:
 5    """使用預編譯正則表達式的 Hook 驗證器"""
 6
 7    # 預編譯的導入模式
 8    HOOK_IO_PATTERNS: List[Pattern] = [
 9        re.compile(r"from\s+hook_io\s+import"),
10        re.compile(r"from\s+lib\.hook_io\s+import"),
11    ]
12
13    HOOK_LOGGING_PATTERNS: List[Pattern] = [
14        re.compile(r"from\s+hook_logging\s+import"),
15        re.compile(r"from\s+lib\.hook_logging\s+import"),
16    ]
17
18    CONFIG_LOADER_PATTERNS: List[Pattern] = [
19        re.compile(r"from\s+config_loader\s+import"),
20        re.compile(r"from\s+lib\.config_loader\s+import"),
21    ]
22
23    GIT_UTILS_PATTERNS: List[Pattern] = [
24        re.compile(r"from\s+git_utils\s+import"),
25        re.compile(r"from\s+lib\.git_utils\s+import"),
26    ]
27
28    # 預編譯的輸出模式
29    OUTPUT_PATTERNS: List[Pattern] = [
30        re.compile(r"write_hook_output\s*\("),
31        re.compile(r"create_pretooluse_output\s*\("),
32        re.compile(r"create_posttooluse_output\s*\("),
33    ]
34
35    BAD_OUTPUT_PATTERNS: List[Pattern] = [
36        re.compile(r'print\s*\(\s*json\.dumps\s*\('),
37        re.compile(r'sys\.stdout\.write\s*\(\s*json\.dumps\s*\('),
38    ]
39
40    # 預編譯的命名模式
41    VALID_NAME_PATTERNS: List[Pattern] = [
42        re.compile(r"^[a-z0-9](/python-advanced/08-practical-optimization/case-studies/regex-precompile/[a-z0-9\-_]*[a-z0-9])?\.py$"),
43    ]
44
45    # 預編譯的 Hook 類型推測模式
46    HOOK_TYPE_HINTS: List[Tuple[str, Pattern]] = [
47        ("PreToolUse", re.compile(r"create_pretooluse_output|permissionDecision")),
48        ("PostToolUse", re.compile(r"create_posttooluse_output|additionalContext")),
49        ("Stop", re.compile(r"Stop|subagent")),
50        ("SessionStart", re.compile(r"SessionStart|session_id")),
51    ]
52
53    # 其他預編譯模式
54    JSON_OUTPUT_PATTERNS: List[Pattern] = [
55        re.compile(r"json\.dumps"),
56        re.compile(r"write_hook_output"),
57        re.compile(r"create_.*_output"),
58    ]

步驟 3:更新匹配方法

修改輔助方法,使用預編譯的 pattern 物件:

 1def _has_import(self, content: str, patterns: List[Pattern]) -> bool:
 2    """檢查是否有符合任一模式的導入(使用預編譯模式)"""
 3    return any(
 4        pattern.search(content)  # 直接使用 Pattern.search()
 5        for pattern in patterns
 6    )
 7
 8def _matches_pattern(self, content: str, patterns: List[Pattern]) -> bool:
 9    """檢查是否符合任一模式(使用預編譯模式)"""
10    return any(
11        pattern.search(content)
12        for pattern in patterns
13    )
14
15def _has_json_output(self, content: str) -> bool:
16    """檢查是否有 JSON 輸出相關程式碼"""
17    return any(
18        pattern.search(content)
19        for pattern in self.JSON_OUTPUT_PATTERNS
20    )

完整程式碼

以下是完整的優化版本:

  1#!/usr/bin/env python3
  2"""
  3Hook 合規性驗證工具(優化版)
  4
  5使用 re.compile 預編譯所有正則表達式,減少重複編譯開銷。
  6"""
  7
  8import re
  9from dataclasses import dataclass, field
 10from pathlib import Path
 11from typing import List, Optional, Pattern, Tuple
 12
 13@dataclass
 14class ValidationIssue:
 15    """驗證問題描述"""
 16    level: str  # "error" | "warning" | "info"
 17    message: str
 18    line: Optional[int] = None
 19    suggestion: Optional[str] = None
 20
 21@dataclass
 22class ValidationResult:
 23    """單個 Hook 的驗證結果"""
 24    hook_path: str
 25    issues: List[ValidationIssue] = field(default_factory=list)
 26    is_compliant: bool = True
 27
 28    def __post_init__(self):
 29        self.is_compliant = not any(
 30            issue.level == "error" for issue in self.issues
 31        )
 32
 33class HookValidatorOptimized:
 34    """
 35    使用預編譯正則表達式的 Hook 驗證器
 36
 37    所有正則表達式在類別定義時編譯一次,
 38    之後所有實例共享這些已編譯的 pattern 物件。
 39    """
 40
 41    # ===== 預編譯的正則表達式 =====
 42
 43    # 導入模式
 44    HOOK_IO_PATTERNS: List[Pattern] = [
 45        re.compile(r"from\s+hook_io\s+import"),
 46        re.compile(r"from\s+lib\.hook_io\s+import"),
 47    ]
 48
 49    HOOK_LOGGING_PATTERNS: List[Pattern] = [
 50        re.compile(r"from\s+hook_logging\s+import"),
 51        re.compile(r"from\s+lib\.hook_logging\s+import"),
 52    ]
 53
 54    CONFIG_LOADER_PATTERNS: List[Pattern] = [
 55        re.compile(r"from\s+config_loader\s+import"),
 56        re.compile(r"from\s+lib\.config_loader\s+import"),
 57    ]
 58
 59    GIT_UTILS_PATTERNS: List[Pattern] = [
 60        re.compile(r"from\s+git_utils\s+import"),
 61        re.compile(r"from\s+lib\.git_utils\s+import"),
 62    ]
 63
 64    # 輸出模式
 65    OUTPUT_PATTERNS: List[Pattern] = [
 66        re.compile(r"write_hook_output\s*\("),
 67        re.compile(r"create_pretooluse_output\s*\("),
 68        re.compile(r"create_posttooluse_output\s*\("),
 69    ]
 70
 71    BAD_OUTPUT_PATTERNS: List[Pattern] = [
 72        re.compile(r'print\s*\(\s*json\.dumps\s*\('),
 73        re.compile(r'sys\.stdout\.write\s*\(\s*json\.dumps\s*\('),
 74    ]
 75
 76    # 命名模式
 77    VALID_NAME_PATTERN: Pattern = re.compile(
 78        r"^[a-z0-9](/python-advanced/08-practical-optimization/case-studies/regex-precompile/[a-z0-9\-_]*[a-z0-9])?\.py$"
 79    )
 80
 81    # Hook 類型推測
 82    HOOK_TYPE_HINTS: List[Tuple[str, Pattern]] = [
 83        ("PreToolUse", re.compile(r"create_pretooluse_output|permissionDecision")),
 84        ("PostToolUse", re.compile(r"create_posttooluse_output|additionalContext")),
 85        ("Stop", re.compile(r"Stop|subagent")),
 86        ("SessionStart", re.compile(r"SessionStart|session_id")),
 87    ]
 88
 89    # JSON 輸出檢測
 90    JSON_OUTPUT_PATTERNS: List[Pattern] = [
 91        re.compile(r"json\.dumps"),
 92        re.compile(r"write_hook_output"),
 93        re.compile(r"create_.*_output"),
 94    ]
 95
 96    # 功能推測模式
 97    CONFIG_KEYWORDS_PATTERN: Pattern = re.compile(
 98        r"load_config|configuration|config|yaml|json",
 99        re.IGNORECASE
100    )
101
102    GIT_KEYWORDS_PATTERN: Pattern = re.compile(
103        r"git|branch|commit|worktree|is_protected_branch|get_current_branch",
104        re.IGNORECASE
105    )
106
107    def __init__(self, project_root: Optional[str] = None):
108        """初始化驗證器"""
109        import os
110        if project_root is None:
111            project_root = os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd())
112        self.project_root = Path(project_root)
113
114    def _has_import(self, content: str, patterns: List[Pattern]) -> bool:
115        """檢查是否有符合任一模式的導入"""
116        return any(pattern.search(content) for pattern in patterns)
117
118    def _matches_pattern(self, content: str, patterns: List[Pattern]) -> bool:
119        """檢查是否符合任一模式"""
120        return any(pattern.search(content) for pattern in patterns)
121
122    def _has_json_output(self, content: str) -> bool:
123        """檢查是否有 JSON 輸出相關程式碼"""
124        return any(
125            pattern.search(content)
126            for pattern in self.JSON_OUTPUT_PATTERNS
127        )
128
129    def _needs_config_loader(self, content: str, hook_path: Optional[Path] = None) -> bool:
130        """判斷 Hook 是否需要配置載入"""
131        if self.CONFIG_KEYWORDS_PATTERN.search(content):
132            return True
133
134        if hook_path:
135            name_lower = hook_path.stem.lower()
136            if any(kw in name_lower for kw in ["config", "agent", "dispatch"]):
137                return True
138
139        return False
140
141    def _needs_git_utils(self, content: str, hook_path: Optional[Path] = None) -> bool:
142        """判斷 Hook 是否需要 Git 操作"""
143        if self.GIT_KEYWORDS_PATTERN.search(content):
144            return True
145
146        if hook_path:
147            name_lower = hook_path.stem.lower()
148            if any(kw in name_lower for kw in ["branch", "git", "commit", "worktree"]):
149                return True
150
151        return False
152
153    def check_naming_convention(self, hook_path: Path) -> List[ValidationIssue]:
154        """檢查命名規範"""
155        issues = []
156        filename = hook_path.name
157
158        if not self.VALID_NAME_PATTERN.match(filename):
159            issues.append(
160                ValidationIssue(
161                    level="warning",
162                    message=f"檔案名稱不符合規範: {filename}",
163                    suggestion="建議使用 snake-case 或 kebab-case 命名"
164                )
165            )
166
167        return issues
168
169    def infer_hook_type(self, content: str) -> Optional[str]:
170        """根據內容推測 Hook 類型"""
171        for hook_type, pattern in self.HOOK_TYPE_HINTS:
172            if pattern.search(content):
173                return hook_type
174        return None
175
176    def validate_hook(self, hook_path: str) -> ValidationResult:
177        """驗證單個 Hook 檔案"""
178        path = Path(hook_path)
179        if not path.is_absolute():
180            path = self.project_root / path
181
182        if not path.exists():
183            return ValidationResult(
184                hook_path=str(path),
185                issues=[ValidationIssue(
186                    level="error",
187                    message=f"Hook 檔案不存在: {path}"
188                )]
189            )
190
191        try:
192            content = path.read_text(encoding="utf-8")
193        except Exception as e:
194            return ValidationResult(
195                hook_path=str(path),
196                issues=[ValidationIssue(
197                    level="error",
198                    message=f"無法讀取檔案: {e}"
199                )]
200            )
201
202        issues = []
203        issues.extend(self.check_naming_convention(path))
204
205        # 使用預編譯模式進行各項檢查
206        if not self._has_import(content, self.HOOK_IO_PATTERNS):
207            issues.append(ValidationIssue(
208                level="warning",
209                message="未導入 hook_io 模組"
210            ))
211
212        if self._matches_pattern(content, self.BAD_OUTPUT_PATTERNS):
213            issues.append(ValidationIssue(
214                level="warning",
215                message="使用不推薦的輸出方式"
216            ))
217
218        return ValidationResult(hook_path=str(path), issues=issues)

效能測量

使用 timeit 模組來精確測量預編譯帶來的效能提升:

  1#!/usr/bin/env python3
  2"""
  3正則表達式預編譯效能測試
  4
  5比較:
  61. 每次使用 re.search(string_pattern, content)
  72. 使用預編譯的 pattern.search(content)
  8"""
  9
 10import re
 11import timeit
 12from typing import List, Pattern
 13
 14# 測試用的正則表達式模式(來自 hook_validator.py)
 15STRING_PATTERNS = [
 16    r"from\s+hook_io\s+import",
 17    r"from\s+lib\.hook_io\s+import",
 18    r"from\s+hook_logging\s+import",
 19    r"from\s+lib\.hook_logging\s+import",
 20    r"from\s+config_loader\s+import",
 21    r"from\s+lib\.config_loader\s+import",
 22    r"from\s+git_utils\s+import",
 23    r"from\s+lib\.git_utils\s+import",
 24    r"write_hook_output\s*\(",
 25    r"create_pretooluse_output\s*\(",
 26    r"create_posttooluse_output\s*\(",
 27    r'print\s*\(\s*json\.dumps\s*\(',
 28    r'sys\.stdout\.write\s*\(\s*json\.dumps\s*\(',
 29    r"^[a-z0-9](/python-advanced/08-practical-optimization/case-studies/regex-precompile/[a-z0-9\-_]*[a-z0-9])?\.py$",
 30]
 31
 32# 預編譯版本
 33COMPILED_PATTERNS: List[Pattern] = [
 34    re.compile(p) for p in STRING_PATTERNS
 35]
 36
 37# 模擬的 Hook 檔案內容
 38SAMPLE_CONTENT = '''
 39#!/usr/bin/env python3
 40"""Sample hook for testing"""
 41
 42import json
 43import sys
 44from pathlib import Path
 45
 46from hook_io import read_hook_input, write_hook_output
 47from hook_logging import setup_hook_logging
 48from config_loader import load_config
 49
 50logger = setup_hook_logging(__name__)
 51
 52def main():
 53    """Main entry point"""
 54    input_data = read_hook_input()
 55    config = load_config()
 56
 57    # Process the input
 58    result = process(input_data, config)
 59
 60    # Write output using recommended function
 61    write_hook_output({
 62        "result": "continue",
 63        "additionalContext": result
 64    })
 65
 66def process(data, config):
 67    """Process the hook data"""
 68    return {"status": "ok"}
 69
 70if __name__ == "__main__":
 71    main()
 72'''
 73
 74def search_with_strings(content: str, patterns: List[str]) -> List[bool]:
 75    """使用字串模式搜尋(每次都會經過 re 快取)"""
 76    return [
 77        bool(re.search(pattern, content))
 78        for pattern in patterns
 79    ]
 80
 81def search_with_compiled(content: str, patterns: List[Pattern]) -> List[bool]:
 82    """使用預編譯模式搜尋"""
 83    return [
 84        bool(pattern.search(content))
 85        for pattern in patterns
 86    ]
 87
 88def benchmark():
 89    """執行效能測試"""
 90    # 預熱(讓 re 模組的快取填滿)
 91    for _ in range(100):
 92        search_with_strings(SAMPLE_CONTENT, STRING_PATTERNS)
 93        search_with_compiled(SAMPLE_CONTENT, COMPILED_PATTERNS)
 94
 95    # 測試參數
 96    iterations = 10000
 97    repeat = 5
 98
 99    # 測試字串模式(有 re 快取)
100    string_times = timeit.repeat(
101        lambda: search_with_strings(SAMPLE_CONTENT, STRING_PATTERNS),
102        number=iterations,
103        repeat=repeat
104    )
105
106    # 測試預編譯模式
107    compiled_times = timeit.repeat(
108        lambda: search_with_compiled(SAMPLE_CONTENT, COMPILED_PATTERNS),
109        number=iterations,
110        repeat=repeat
111    )
112
113    # 計算結果
114    string_best = min(string_times)
115    compiled_best = min(compiled_times)
116    speedup = string_best / compiled_best
117
118    # 輸出結果
119    print("正則表達式預編譯效能測試")
120    print("=" * 60)
121    print(f"測試內容大小: {len(SAMPLE_CONTENT)} 字元")
122    print(f"模式數量: {len(STRING_PATTERNS)} 個")
123    print(f"迭代次數: {iterations:,} 次 x {repeat} 輪")
124    print()
125    print("結果(最佳時間):")
126    print("-" * 60)
127    print(f"字串模式 (re.search):     {string_best:.4f} 秒")
128    print(f"預編譯模式 (Pattern):     {compiled_best:.4f} 秒")
129    print(f"加速比:                   {speedup:.2f}x")
130    print()
131
132    # 單次操作時間
133    string_per_op = (string_best / iterations) * 1_000_000  # 微秒
134    compiled_per_op = (compiled_best / iterations) * 1_000_000
135
136    print("單次操作時間:")
137    print("-" * 60)
138    print(f"字串模式:                 {string_per_op:.2f} 微秒")
139    print(f"預編譯模式:               {compiled_per_op:.2f} 微秒")
140    print(f"每次節省:                 {string_per_op - compiled_per_op:.2f} 微秒")
141
142    return {
143        "string_time": string_best,
144        "compiled_time": compiled_best,
145        "speedup": speedup,
146    }
147
148def benchmark_cache_miss():
149    """測試快取未命中的情況"""
150    print("\n" + "=" * 60)
151    print("快取未命中測試(清空快取後)")
152    print("=" * 60)
153
154    iterations = 1000
155
156    # 清空 re 模組快取
157    re.purge()
158
159    # 測試字串模式(快取被清空)
160    start = timeit.default_timer()
161    for _ in range(iterations):
162        re.purge()  # 每次都清空快取
163        search_with_strings(SAMPLE_CONTENT, STRING_PATTERNS)
164    string_time = timeit.default_timer() - start
165
166    # 測試預編譯模式(不受快取影響)
167    start = timeit.default_timer()
168    for _ in range(iterations):
169        re.purge()  # 清空快取不影響預編譯模式
170        search_with_compiled(SAMPLE_CONTENT, COMPILED_PATTERNS)
171    compiled_time = timeit.default_timer() - start
172
173    speedup = string_time / compiled_time
174
175    print(f"字串模式 (無快取):        {string_time:.4f} 秒")
176    print(f"預編譯模式:               {compiled_time:.4f} 秒")
177    print(f"加速比:                   {speedup:.2f}x")
178
179if __name__ == "__main__":
180    results = benchmark()
181    benchmark_cache_miss()

典型測試結果

 1正則表達式預編譯效能測試
 2============================================================
 3測試內容大小: 847 字元
 4模式數量: 14 個
 5迭代次數: 10,000 次 x 5 輪
 6
 7結果(最佳時間):
 8------------------------------------------------------------
 9字串模式 (re.search):     0.4823 秒
10預編譯模式 (Pattern):     0.3891 秒
11加速比:                   1.24x
12
13單次操作時間:
14------------------------------------------------------------
15字串模式:                 48.23 微秒
16預編譯模式:               38.91 微秒
17每次節省:                 9.32 微秒
18
19============================================================
20快取未命中測試(清空快取後)
21============================================================
22字串模式 (無快取):        2.3456 秒
23預編譯模式:               0.3912 秒
24加速比:                   6.00x

從結果可以看出:

  • 正常情況:預編譯帶來約 1.2-1.3 倍 的加速
  • 快取未命中:當 re 模組快取失效時,加速可達 6 倍

設計權衡

面向字串模式預編譯模式
記憶體使用較低(依賴 re 快取)略高(每個 Pattern 物件)
首次載入快(延遲編譯)慢(類別載入時編譯)
執行效能依賴快取狀態穩定且可預測
程式碼可讀性模式定義較簡潔意圖更明確
型別提示List[str]List[Pattern]
適合場景少量模式、低頻呼叫多模式、高頻呼叫

記憶體考量

預編譯的 Pattern 物件會佔用額外記憶體:

 1import re
 2import sys
 3
 4pattern_str = r"from\s+hook_io\s+import"
 5pattern_obj = re.compile(pattern_str)
 6
 7print(f"字串大小: {sys.getsizeof(pattern_str)} bytes")
 8print(f"Pattern 大小: {sys.getsizeof(pattern_obj)} bytes")
 9# 字串大小: 74 bytes
10# Pattern 大小: 256 bytes(視模式複雜度而定)

但在大多數情況下,這點記憶體是值得的。

什麼時候該用這個技術?

適合預編譯的情況

  • 同一個模式會被使用多次(例如在迴圈中)
  • 模式數量較多,可能超過 re 快取上限(512 個)
  • 效能敏感的程式碼路徑
  • 需要穩定、可預測的執行時間
  • 類別或模組級別的模式定義

不需要預編譯的情況

  • 模式只使用一次
  • 快速原型開發
  • 簡單的腳本工具
  • 模式是動態生成的

練習

基礎練習:測量你的正則表達式效能

 1"""
 2練習 1:測量自己專案中正則表達式的效能
 3
 4步驟:
 51. 找出你專案中使用正則表達式的程式碼
 62. 記錄有多少個不同的模式
 73. 測量預編譯前後的效能差異
 8"""
 9
10import re
11import timeit
12
13# TODO: 將你專案中的模式填入這裡
14YOUR_PATTERNS = [
15    r"your_pattern_1",
16    r"your_pattern_2",
17    # ...
18]
19
20YOUR_TEST_CONTENT = """
21your test content here
22"""
23
24def measure_performance():
25    """測量效能差異"""
26    # 字串版本
27    string_patterns = YOUR_PATTERNS
28
29    # 預編譯版本
30    compiled_patterns = [re.compile(p) for p in YOUR_PATTERNS]
31
32    # 測量
33    string_time = timeit.timeit(
34        lambda: [re.search(p, YOUR_TEST_CONTENT) for p in string_patterns],
35        number=10000
36    )
37
38    compiled_time = timeit.timeit(
39        lambda: [p.search(YOUR_TEST_CONTENT) for p in compiled_patterns],
40        number=10000
41    )
42
43    print(f"字串模式: {string_time:.4f} 秒")
44    print(f"預編譯模式: {compiled_time:.4f} 秒")
45    print(f"加速比: {string_time / compiled_time:.2f}x")
46
47if __name__ == "__main__":
48    measure_performance()

進階練習:監控 re 快取狀態

 1"""
 2練習 2:監控 re 模組的快取狀態
 3
 4了解你的程式實際使用了多少快取空間。
 5"""
 6
 7import re
 8
 9def check_cache_status():
10    """檢查 re 模組快取狀態"""
11    print(f"快取上限: {re._MAXCACHE}")
12    print(f"目前快取數量: {len(re._cache)}")
13    print(f"使用率: {len(re._cache) / re._MAXCACHE * 100:.1f}%")
14
15    if len(re._cache) > re._MAXCACHE * 0.8:
16        print("警告:快取即將滿載!")
17
18def simulate_cache_overflow():
19    """模擬快取溢出"""
20    print("模擬快取溢出...")
21
22    # 記錄初始狀態
23    initial_count = len(re._cache)
24
25    # 建立大量不同的模式
26    for i in range(600):
27        re.search(f"pattern_{i}", "test content")
28
29    final_count = len(re._cache)
30
31    print(f"初始快取: {initial_count}")
32    print(f"最終快取: {final_count}")
33    print(f"快取被清空了 {(600 - final_count) // re._MAXCACHE} 次")
34
35if __name__ == "__main__":
36    check_cache_status()
37    print()
38    simulate_cache_overflow()

挑戰題:建立自動預編譯裝飾器

 1"""
 2練習 3:建立自動預編譯裝飾器
 3
 4設計一個裝飾器,自動將類別中的字串模式轉換為預編譯模式。
 5"""
 6
 7import re
 8from typing import List, Pattern, Type
 9
10def auto_compile_patterns(cls: Type) -> Type:
11    """
12    類別裝飾器:自動預編譯所有 _PATTERNS 結尾的類別屬性
13
14    使用方式:
15        @auto_compile_patterns
16        class MyValidator:
17            IMPORT_PATTERNS = [
18                r"from\s+module\s+import",
19                r"import\s+module",
20            ]
21    """
22    for attr_name in dir(cls):
23        if attr_name.endswith("_PATTERNS") and not attr_name.startswith("_"):
24            value = getattr(cls, attr_name)
25
26            if isinstance(value, list) and value and isinstance(value[0], str):
27                # 將字串列表轉換為預編譯模式列表
28                compiled = [re.compile(p) for p in value]
29                setattr(cls, attr_name, compiled)
30                print(f"預編譯 {cls.__name__}.{attr_name}: {len(compiled)} 個模式")
31
32    return cls
33
34# 測試
35@auto_compile_patterns
36class TestValidator:
37    IMPORT_PATTERNS = [
38        r"from\s+module\s+import",
39        r"import\s+module",
40    ]
41
42    OUTPUT_PATTERNS = [
43        r"print\s*\(",
44        r"write\s*\(",
45    ]
46
47    NOT_A_PATTERN = "這不是模式列表"
48
49if __name__ == "__main__":
50    # 驗證轉換結果
51    print(f"\nIMPORT_PATTERNS 類型: {type(TestValidator.IMPORT_PATTERNS[0])}")
52    print(f"OUTPUT_PATTERNS 類型: {type(TestValidator.OUTPUT_PATTERNS[0])}")
53    print(f"NOT_A_PATTERN 類型: {type(TestValidator.NOT_A_PATTERN)}")

延伸閱讀


上一章:並行 Hook 驗證 下一章:LRU 快取