案例:正則表達式預編譯
案例:正則表達式預編譯
本案例基於 .claude/lib/hook_validator.py 的實際程式碼,展示如何透過正則表達式預編譯來減少重複編譯的開銷。
先備知識
- 模組八基礎章節
- Python
re模組基本操作 - 基本的效能測量概念
問題背景
現有設計
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 會在內部執行兩個步驟:
- 編譯:將正則表達式字串轉換為內部的 pattern 物件
- 匹配:使用 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 快取