本案例基於 .claude/lib/hook_validator.py 的實際程式碼,展示如何用 Metaclass 實現檢查器的自動註冊。

先備知識

問題背景

現有設計

hook_validator.pyHookValidator 類別包含多個 check_* 方法,需要在 validate_hook() 中手動呼叫:

 1class HookValidator:
 2    """Hook 合規性驗證器"""
 3
 4    def validate_hook(self, hook_path: str) -> ValidationResult:
 5        """驗證單個 Hook 檔案"""
 6        # ... 前置處理 ...
 7
 8        # 執行各項檢查 - 手動呼叫每個 check 方法
 9        issues = []
10        issues.extend(self.check_naming_convention(hook_path))
11        issues.extend(self.check_lib_imports(content, hook_path))
12        issues.extend(self.check_output_format(content))
13        issues.extend(self.check_test_exists(hook_path))
14
15        return ValidationResult(hook_path=str(hook_path), issues=issues)
16
17    def check_naming_convention(self, hook_path: Path) -> List[ValidationIssue]:
18        """檢查命名規範"""
19        # ... 實作 ...
20
21    def check_lib_imports(self, content: str, hook_path: Path) -> List[ValidationIssue]:
22        """檢查共用模組導入"""
23        # ... 實作 ...
24
25    def check_output_format(self, content: str) -> List[ValidationIssue]:
26        """檢查 Hook 輸出格式"""
27        # ... 實作 ...
28
29    def check_test_exists(self, hook_path: Path) -> List[ValidationIssue]:
30        """檢查對應的測試檔案是否存在"""
31        # ... 實作 ...

這個設計的優點

  • 明確的執行順序:可以精確控制檢查項的執行順序
  • 容易理解呼叫流程:閱讀 validate_hook() 就知道會執行哪些檢查
  • 簡單直覺:不需要學習額外的抽象概念

這個設計的限制

新增檢查項時:

  1. 需要修改兩處程式碼:新增 check_* 方法 + 修改 validate_hook() 呼叫
  2. 容易忘記註冊:新增方法後忘記在 validate_hook() 中呼叫
  3. 無法動態控制:無法在執行時期啟用/停用特定檢查項
  4. 難以擴展:子類別新增檢查項也需要覆寫 validate_hook()

進階解決方案

設計目標

  1. 定義檢查方法時自動註冊到執行清單
  2. 支援優先順序控制
  3. 支援動態啟用/停用特定檢查項
  4. 子類別的檢查項自動繼承

實作步驟

步驟 1:用裝飾器標記檢查方法

首先,我們需要一個方式來標記哪些方法是「檢查器」。裝飾器是最自然的選擇:

 1from functools import wraps
 2from typing import Callable, Optional
 3
 4def check(
 5    priority: int = 100,
 6    enabled: bool = True,
 7    description: str = ""
 8) -> Callable:
 9    """
10    Decorator to mark a method as a checker.
11
12    Args:
13        priority: Execution order (lower runs first)
14        enabled: Whether this check is enabled by default
15        description: Human-readable description
16
17    Example:
18        @check(priority=10, description="Validate filename format")
19        def check_naming(self, hook_path):
20            ...
21    """
22    def decorator(func: Callable) -> Callable:
23        # Store metadata on the function object
24        func._is_checker = True
25        func._checker_priority = priority
26        func._checker_enabled = enabled
27        func._checker_description = description or func.__doc__ or func.__name__
28
29        @wraps(func)
30        def wrapper(*args, **kwargs):
31            return func(*args, **kwargs)
32
33        # Copy metadata to wrapper
34        wrapper._is_checker = True
35        wrapper._checker_priority = priority
36        wrapper._checker_enabled = enabled
37        wrapper._checker_description = description or func.__doc__ or func.__name__
38
39        return wrapper
40    return decorator

使用方式:

 1class Validator:
 2    @check(priority=10, description="Check filename format")
 3    def check_naming_convention(self, hook_path):
 4        """檢查命名規範"""
 5        ...
 6
 7    @check(priority=20, description="Check library imports")
 8    def check_lib_imports(self, content, hook_path):
 9        """檢查共用模組導入"""
10        ...

步驟 2:用 Metaclass 收集標記的方法

接下來,用 Metaclass 在類別建立時自動收集所有被 @check 標記的方法:

 1class CheckerMeta(type):
 2    """
 3    Metaclass that automatically collects methods marked with @check.
 4
 5    When a class is created, this metaclass:
 6    1. Scans all methods for the _is_checker attribute
 7    2. Collects them into a _checkers registry
 8    3. Sorts by priority for execution order
 9    """
10
11    def __new__(mcs, name: str, bases: tuple, namespace: dict):
12        # Create the class first
13        cls = super().__new__(mcs, name, bases, namespace)
14
15        # Collect checkers from parent classes
16        inherited_checkers = {}
17        for base in bases:
18            if hasattr(base, '_checkers'):
19                inherited_checkers.update(base._checkers)
20
21        # Collect checkers from current class
22        current_checkers = {}
23        for attr_name, attr_value in namespace.items():
24            if callable(attr_value) and getattr(attr_value, '_is_checker', False):
25                current_checkers[attr_name] = {
26                    'method': attr_name,
27                    'priority': attr_value._checker_priority,
28                    'enabled': attr_value._checker_enabled,
29                    'description': attr_value._checker_description,
30                }
31
32        # Merge: current class can override parent checkers
33        all_checkers = {**inherited_checkers, **current_checkers}
34
35        # Store as class attribute
36        cls._checkers = all_checkers
37
38        return cls

步驟 3:實作優先順序

在 Metaclass 中已經收集了優先順序資訊,現在需要一個方法按順序執行:

 1class CheckerBase(metaclass=CheckerMeta):
 2    """
 3    Base class for validators with auto-registration.
 4
 5    Provides run_all_checks() to execute registered checkers.
 6    """
 7
 8    def get_sorted_checkers(self) -> list:
 9        """
10        Get all enabled checkers sorted by priority.
11
12        Returns:
13            List of (method_name, checker_info) tuples
14        """
15        checkers = [
16            (name, info)
17            for name, info in self._checkers.items()
18            if info['enabled']
19        ]
20        # Sort by priority (lower number = higher priority)
21        return sorted(checkers, key=lambda x: x[1]['priority'])
22
23    def run_all_checks(self, *args, **kwargs) -> list:
24        """
25        Run all enabled checkers in priority order.
26
27        Args:
28            *args, **kwargs: Arguments passed to each checker
29
30        Returns:
31            Combined list of issues from all checkers
32        """
33        all_issues = []
34
35        for method_name, info in self.get_sorted_checkers():
36            method = getattr(self, method_name)
37            try:
38                issues = method(*args, **kwargs)
39                if issues:
40                    all_issues.extend(issues)
41            except Exception as e:
42                # Optionally handle checker errors
43                all_issues.append({
44                    'level': 'error',
45                    'message': f"Checker {method_name} failed: {e}"
46                })
47
48        return all_issues

步驟 4:實作啟用/停用機制

允許在執行時期動態控制檢查項:

 1class CheckerBase(metaclass=CheckerMeta):
 2    """
 3    Base class for validators with auto-registration.
 4    """
 5
 6    def __init__(self):
 7        # Instance-level override for checker states
 8        self._checker_overrides = {}
 9
10    def enable_checker(self, checker_name: str) -> None:
11        """
12        Enable a specific checker for this instance.
13
14        Args:
15            checker_name: Name of the checker method
16        """
17        if checker_name not in self._checkers:
18            raise ValueError(f"Unknown checker: {checker_name}")
19        self._checker_overrides[checker_name] = True
20
21    def disable_checker(self, checker_name: str) -> None:
22        """
23        Disable a specific checker for this instance.
24
25        Args:
26            checker_name: Name of the checker method
27        """
28        if checker_name not in self._checkers:
29            raise ValueError(f"Unknown checker: {checker_name}")
30        self._checker_overrides[checker_name] = False
31
32    def is_checker_enabled(self, checker_name: str) -> bool:
33        """
34        Check if a specific checker is enabled.
35
36        Instance overrides take precedence over class defaults.
37        """
38        if checker_name in self._checker_overrides:
39            return self._checker_overrides[checker_name]
40        return self._checkers.get(checker_name, {}).get('enabled', False)
41
42    def get_sorted_checkers(self) -> list:
43        """Get all enabled checkers sorted by priority."""
44        checkers = [
45            (name, info)
46            for name, info in self._checkers.items()
47            if self.is_checker_enabled(name)
48        ]
49        return sorted(checkers, key=lambda x: x[1]['priority'])
50
51    def list_checkers(self) -> list:
52        """
53        List all available checkers with their status.
54
55        Returns:
56            List of dicts with checker information
57        """
58        result = []
59        for name, info in sorted(
60            self._checkers.items(),
61            key=lambda x: x[1]['priority']
62        ):
63            result.append({
64                'name': name,
65                'priority': info['priority'],
66                'enabled': self.is_checker_enabled(name),
67                'description': info['description'],
68            })
69        return result

完整程式碼

  1#!/usr/bin/env python3
  2"""
  3Auto-registration pattern using Metaclass.
  4
  5This module demonstrates how to automatically register checker methods
  6using a combination of decorators and metaclasses.
  7"""
  8
  9from dataclasses import dataclass, field
 10from functools import wraps
 11from pathlib import Path
 12from typing import Callable, List, Optional, Any
 13import re
 14
 15# ===== Data Classes =====
 16
 17@dataclass
 18class ValidationIssue:
 19    """Represents a validation issue."""
 20    level: str  # "error" | "warning" | "info"
 21    message: str
 22    line: Optional[int] = None
 23    suggestion: Optional[str] = None
 24
 25@dataclass
 26class ValidationResult:
 27    """Validation result for a single target."""
 28    target: str
 29    issues: List[ValidationIssue] = field(default_factory=list)
 30
 31    @property
 32    def is_valid(self) -> bool:
 33        """True if no errors found."""
 34        return not any(issue.level == "error" for issue in self.issues)
 35
 36# ===== Decorator =====
 37
 38def check(
 39    priority: int = 100,
 40    enabled: bool = True,
 41    description: str = ""
 42) -> Callable:
 43    """
 44    Decorator to mark a method as a checker.
 45
 46    Args:
 47        priority: Execution order (lower runs first)
 48        enabled: Whether this check is enabled by default
 49        description: Human-readable description
 50    """
 51    def decorator(func: Callable) -> Callable:
 52        @wraps(func)
 53        def wrapper(*args, **kwargs):
 54            return func(*args, **kwargs)
 55
 56        # Store metadata
 57        wrapper._is_checker = True
 58        wrapper._checker_priority = priority
 59        wrapper._checker_enabled = enabled
 60        wrapper._checker_description = description or func.__doc__ or func.__name__
 61
 62        return wrapper
 63    return decorator
 64
 65# ===== Metaclass =====
 66
 67class CheckerMeta(type):
 68    """
 69    Metaclass that automatically collects @check decorated methods.
 70    """
 71
 72    def __new__(mcs, name: str, bases: tuple, namespace: dict):
 73        cls = super().__new__(mcs, name, bases, namespace)
 74
 75        # Inherit checkers from parent classes
 76        inherited_checkers = {}
 77        for base in bases:
 78            if hasattr(base, '_checkers'):
 79                inherited_checkers.update(base._checkers)
 80
 81        # Collect checkers from current class
 82        current_checkers = {}
 83        for attr_name, attr_value in namespace.items():
 84            if callable(attr_value) and getattr(attr_value, '_is_checker', False):
 85                current_checkers[attr_name] = {
 86                    'method': attr_name,
 87                    'priority': attr_value._checker_priority,
 88                    'enabled': attr_value._checker_enabled,
 89                    'description': attr_value._checker_description,
 90                }
 91
 92        # Merge checkers
 93        cls._checkers = {**inherited_checkers, **current_checkers}
 94
 95        return cls
 96
 97# ===== Base Class =====
 98
 99class CheckerBase(metaclass=CheckerMeta):
100    """
101    Base class providing auto-registration functionality.
102    """
103
104    def __init__(self):
105        self._checker_overrides = {}
106
107    def enable_checker(self, name: str) -> None:
108        """Enable a checker for this instance."""
109        if name not in self._checkers:
110            raise ValueError(f"Unknown checker: {name}")
111        self._checker_overrides[name] = True
112
113    def disable_checker(self, name: str) -> None:
114        """Disable a checker for this instance."""
115        if name not in self._checkers:
116            raise ValueError(f"Unknown checker: {name}")
117        self._checker_overrides[name] = False
118
119    def is_checker_enabled(self, name: str) -> bool:
120        """Check if a checker is enabled."""
121        if name in self._checker_overrides:
122            return self._checker_overrides[name]
123        return self._checkers.get(name, {}).get('enabled', False)
124
125    def get_sorted_checkers(self) -> list:
126        """Get enabled checkers sorted by priority."""
127        return sorted(
128            [(n, i) for n, i in self._checkers.items() if self.is_checker_enabled(n)],
129            key=lambda x: x[1]['priority']
130        )
131
132    def list_checkers(self) -> list:
133        """List all checkers with their status."""
134        return [
135            {
136                'name': name,
137                'priority': info['priority'],
138                'enabled': self.is_checker_enabled(name),
139                'description': info['description'],
140            }
141            for name, info in sorted(
142                self._checkers.items(),
143                key=lambda x: x[1]['priority']
144            )
145        ]
146
147# ===== Implementation Example =====
148
149class HookValidator(CheckerBase):
150    """
151    Hook validator with auto-registered checkers.
152
153    Checkers are automatically discovered and executed in priority order.
154    """
155
156    # Pattern constants
157    VALID_NAME_PATTERN = r"^[a-z0-9](/python-advanced/02-metaprogramming/case-studies/auto-registration/[a-z0-9\-_]*[a-z0-9])?\.py$"
158    HOOK_IO_PATTERNS = [
159        r"from\s+hook_io\s+import",
160        r"from\s+lib\.hook_io\s+import",
161    ]
162    BAD_OUTPUT_PATTERNS = [
163        r'print\s*\(\s*json\.dumps\s*\(',
164    ]
165
166    def __init__(self, project_root: Optional[str] = None):
167        super().__init__()
168        self.project_root = Path(project_root) if project_root else Path.cwd()
169
170    def validate(self, hook_path: str) -> ValidationResult:
171        """
172        Validate a hook file by running all enabled checkers.
173
174        Args:
175            hook_path: Path to the hook file
176
177        Returns:
178            ValidationResult with all issues found
179        """
180        path = Path(hook_path)
181        if not path.is_absolute():
182            path = self.project_root / path
183
184        # Read file content
185        try:
186            content = path.read_text(encoding='utf-8')
187        except Exception as e:
188            return ValidationResult(
189                target=str(path),
190                issues=[ValidationIssue(
191                    level="error",
192                    message=f"Cannot read file: {e}"
193                )]
194            )
195
196        # Prepare context for checkers
197        context = {
198            'path': path,
199            'content': content,
200            'filename': path.name,
201        }
202
203        # Run all enabled checkers
204        all_issues = []
205        for method_name, _ in self.get_sorted_checkers():
206            method = getattr(self, method_name)
207            try:
208                issues = method(context)
209                if issues:
210                    all_issues.extend(issues)
211            except Exception as e:
212                all_issues.append(ValidationIssue(
213                    level="error",
214                    message=f"Checker '{method_name}' crashed: {e}"
215                ))
216
217        return ValidationResult(target=str(path), issues=all_issues)
218
219    @check(priority=10, description="Check filename format")
220    def check_naming_convention(self, ctx: dict) -> List[ValidationIssue]:
221        """Validate hook filename follows naming convention."""
222        issues = []
223        filename = ctx['filename']
224
225        if not re.match(self.VALID_NAME_PATTERN, filename):
226            issues.append(ValidationIssue(
227                level="warning",
228                message=f"Filename '{filename}' doesn't follow naming convention",
229                suggestion="Use snake_case or kebab-case, e.g., check_format.py"
230            ))
231
232        return issues
233
234    @check(priority=20, description="Check hook_io import")
235    def check_lib_imports(self, ctx: dict) -> List[ValidationIssue]:
236        """Check if hook_io module is properly imported."""
237        issues = []
238        content = ctx['content']
239
240        has_import = any(
241            re.search(pattern, content)
242            for pattern in self.HOOK_IO_PATTERNS
243        )
244
245        if not has_import:
246            issues.append(ValidationIssue(
247                level="warning",
248                message="hook_io module not imported",
249                suggestion="Add: from hook_io import read_hook_input, write_hook_output"
250            ))
251
252        return issues
253
254    @check(priority=30, description="Check output format")
255    def check_output_format(self, ctx: dict) -> List[ValidationIssue]:
256        """Check if proper output functions are used."""
257        issues = []
258        content = ctx['content']
259
260        for pattern in self.BAD_OUTPUT_PATTERNS:
261            if re.search(pattern, content):
262                issues.append(ValidationIssue(
263                    level="warning",
264                    message="Using print(json.dumps(...)) instead of write_hook_output()",
265                    suggestion="Replace with: write_hook_output(output_dict)"
266                ))
267                break
268
269        return issues
270
271    @check(priority=40, enabled=False, description="Check test file exists")
272    def check_test_exists(self, ctx: dict) -> List[ValidationIssue]:
273        """Check if corresponding test file exists."""
274        issues = []
275        hook_name = ctx['path'].stem
276        test_name = f"test_{hook_name.replace('-', '_')}.py"
277
278        test_path = self.project_root / ".claude" / "lib" / "tests" / test_name
279        if not test_path.exists():
280            issues.append(ValidationIssue(
281                level="info",
282                message=f"No test file found: {test_name}",
283                suggestion=f"Create test at: {test_path}"
284            ))
285
286        return issues
287
288# ===== Extended Example: Subclass =====
289
290class StrictHookValidator(HookValidator):
291    """
292    Extended validator with additional checks.
293
294    Inherits all checks from HookValidator and adds more.
295    """
296
297    @check(priority=15, description="Check shebang line")
298    def check_shebang(self, ctx: dict) -> List[ValidationIssue]:
299        """Check if file starts with proper shebang."""
300        issues = []
301        content = ctx['content']
302
303        if not content.startswith('#!/usr/bin/env python'):
304            issues.append(ValidationIssue(
305                level="info",
306                message="Missing shebang line",
307                suggestion="Add: #!/usr/bin/env python3"
308            ))
309
310        return issues
311
312    @check(priority=25, description="Check docstring exists")
313    def check_docstring(self, ctx: dict) -> List[ValidationIssue]:
314        """Check if module has a docstring."""
315        issues = []
316        content = ctx['content']
317
318        # Simple check: look for triple quotes near the start
319        lines = content.split('\n')[:10]  # First 10 lines
320        has_docstring = any('"""' in line or "'''" in line for line in lines)
321
322        if not has_docstring:
323            issues.append(ValidationIssue(
324                level="info",
325                message="Module docstring not found",
326                suggestion="Add a docstring at the top of the file"
327            ))
328
329        return issues
330
331# ===== Demo =====
332
333if __name__ == "__main__":
334    print("=" * 60)
335    print("Auto-Registration Demo")
336    print("=" * 60)
337
338    # Create validator
339    validator = HookValidator()
340
341    # List all registered checkers
342    print("\n[Registered Checkers]")
343    for checker in validator.list_checkers():
344        status = "ON" if checker['enabled'] else "OFF"
345        print(f"  [{status}] {checker['name']} (priority: {checker['priority']})")
346        print(f"        {checker['description']}")
347
348    # Enable disabled checker
349    print("\n[Enable check_test_exists]")
350    validator.enable_checker('check_test_exists')
351    for checker in validator.list_checkers():
352        if checker['name'] == 'check_test_exists':
353            status = "ON" if checker['enabled'] else "OFF"
354            print(f"  [{status}] {checker['name']}")
355
356    # Demo with extended validator
357    print("\n[Extended Validator - StrictHookValidator]")
358    strict_validator = StrictHookValidator()
359    for checker in strict_validator.list_checkers():
360        status = "ON" if checker['enabled'] else "OFF"
361        print(f"  [{status}] {checker['name']} (priority: {checker['priority']})")
362
363    print("\n" + "=" * 60)

使用範例

 1# Create validator instance
 2validator = HookValidator(project_root="/path/to/project")
 3
 4# List available checkers
 5for checker in validator.list_checkers():
 6    print(f"[{'ON' if checker['enabled'] else 'OFF'}] {checker['name']}")
 7
 8# Output:
 9# [ON] check_naming_convention
10# [ON] check_lib_imports
11# [ON] check_output_format
12# [OFF] check_test_exists
13
14# Enable/disable checkers dynamically
15validator.enable_checker('check_test_exists')
16validator.disable_checker('check_output_format')
17
18# Validate a hook file
19result = validator.validate(".claude/hooks/my-hook.py")
20print(f"Valid: {result.is_valid}")
21for issue in result.issues:
22    print(f"  [{issue.level}] {issue.message}")
23
24# Create extended validator with more checks
25strict = StrictHookValidator()
26print(f"Total checkers: {len(strict.list_checkers())}")  # Inherits parent's checkers

替代方案:__init_subclass__

Python 3.6 引入了 __init_subclass__,可以在不使用 Metaclass 的情況下實現部分功能:

 1class CheckerBase:
 2    """
 3    Base class using __init_subclass__ for auto-registration.
 4
 5    Simpler than metaclass, but less powerful.
 6    """
 7    _checkers = {}
 8
 9    def __init_subclass__(cls, **kwargs):
10        super().__init_subclass__(**kwargs)
11
12        # Collect checkers from this subclass
13        for attr_name in dir(cls):
14            attr = getattr(cls, attr_name, None)
15            if callable(attr) and getattr(attr, '_is_checker', False):
16                cls._checkers[attr_name] = {
17                    'method': attr_name,
18                    'priority': attr._checker_priority,
19                    'enabled': attr._checker_enabled,
20                    'description': attr._checker_description,
21                }
22
23class HookValidator(CheckerBase):
24    """Validator using __init_subclass__."""
25
26    @check(priority=10)
27    def check_naming(self, ctx):
28        """Check naming convention."""
29        pass
30
31    @check(priority=20)
32    def check_imports(self, ctx):
33        """Check imports."""
34        pass

__init_subclass__ vs Metaclass

面向__init_subclass__Metaclass
複雜度
適用場景子類別註冊完整控制類別建立
效能較好略差
修改類別有限完整
繼承處理需手動自動
推薦情況優先使用需要進階功能時

原則:如果 __init_subclass__ 能滿足需求,優先使用它。

設計權衡

面向手動註冊Metaclass 自動註冊
程式碼重複高(每次新增都要改兩處)低(只需加裝飾器)
理解難度中(需理解 Metaclass)
擴展性差(子類別需覆寫)好(自動繼承)
除錯難度中(執行流程隱含)
動態控制需額外實作內建支援
執行順序明確可見由 priority 決定

什麼時候該用這個技術?

適合使用:

  • 插件系統:需要自動發現和載入插件
  • 框架開發:Django admin、pytest fixtures 等
  • 大量相似元件:多個檢查器/處理器需統一管理
  • 需要動態控制:執行時期啟用/停用功能

不建議使用:

  • 只有少數幾個檢查項:手動維護更簡單
  • 團隊不熟悉 Metaclass:增加維護負擔
  • 簡單的應用程式:過度工程
  • 執行順序非常重要:手動呼叫更明確

練習

基礎練習

實作一個簡單的命令註冊系統:

 1class CommandRegistry:
 2    """
 3    命令註冊系統
 4
 5    需求:
 6    1. 用 @command(name="xxx") 裝飾器註冊命令
 7    2. 提供 execute(name, *args) 執行指定命令
 8    3. 提供 list_commands() 列出所有命令
 9    """
10    pass
11
12# 使用範例
13class MyApp(CommandRegistry):
14    @command(name="greet")
15    def say_hello(self, name):
16        return f"Hello, {name}!"
17
18    @command(name="add")
19    def add_numbers(self, a, b):
20        return a + b
21
22app = MyApp()
23print(app.execute("greet", "World"))  # Hello, World!
24print(app.list_commands())  # ['add', 'greet']

進階練習

新增檢查項的相依性管理:

1@check(priority=10, depends_on=['check_file_exists'])
2def check_content(self, ctx):
3    """只有在檔案存在檢查通過後才執行"""
4    pass

提示:

  1. @check 裝飾器加入 depends_on 參數
  2. run_all_checks() 中追蹤每個檢查項的結果
  3. 檢查相依項是否通過再執行

挑戰題

實作跨模組的檢查項發現(類似 pytest):

 1# checkers/naming.py
 2@check(priority=10)
 3def check_naming(ctx):
 4    pass
 5
 6# checkers/imports.py
 7@check(priority=20)
 8def check_imports(ctx):
 9    pass
10
11# main.py
12validator = HookValidator()
13validator.discover_checkers('checkers/')  # 自動載入目錄下的所有檢查項

提示:

  1. 使用 importlib 動態載入模組
  2. 掃描模組中帶有 _is_checker 標記的函式
  3. 將函式綁定為實例方法

延伸閱讀


上一章:宣告式驗證 下一章:類似 Django Field