案例:類似 Django Field 的設計
案例:類似 Django Field 的設計
1. 實作
3. 實作巢狀 Model(
本案例基於 .claude/lib/hook_io.py 的實際程式碼,展示如何結合 Descriptor 和 dataclass 設計類似 Django Model Field 的宣告式 API。
先備知識
問題背景
現有設計
hook_io.py 使用函式工廠模式建構 Hook 輸出:
1def create_pretooluse_output(
2 decision: str,
3 reason: str,
4 user_prompt: Optional[str] = None,
5 system_message: Optional[str] = None,
6 suppress_output: bool = False
7) -> dict:
8 """
9 建立 PreToolUse Hook 輸出格式
10
11 Args:
12 decision: 決策結果 ("allow" | "deny" | "ask")
13 reason: 決策原因說明
14 user_prompt: 詢問用戶的訊息(僅當 decision 為 "ask" 時使用)
15 system_message: 系統訊息(可選)
16 suppress_output: 是否抑制輸出(預設 False)
17
18 Returns:
19 dict: 標準 PreToolUse Hook 輸出格式
20 """
21 output: dict[str, Any] = {
22 "hookSpecificOutput": {
23 "hookEventName": "PreToolUse",
24 "permissionDecision": decision,
25 "permissionDecisionReason": reason
26 }
27 }
28
29 if user_prompt:
30 output["hookSpecificOutput"]["userPrompt"] = user_prompt
31
32 if system_message:
33 output["systemMessage"] = system_message
34
35 if suppress_output:
36 output["suppressOutput"] = True
37
38 return output這個設計的優點
- 清晰的建構流程:函式簽名清楚說明所需參數
- 支援可選參數:使用
Optional和預設值處理可選欄位 - 型別提示完整:有完整的參數型別標註
這個設計的限制
當需要序列化/反序列化時:
- 需要手動處理每個欄位:to_dict 和 from_dict 需要逐一處理
- 欄位定義與驗證分離:型別檢查和業務驗證在不同地方
- 難以生成文件或 schema:無法自動產生 JSON Schema 或 API 文件
進階解決方案
設計目標
- 欄位定義包含型別、驗證、序列化
- 自動生成
__init__、to_dict、from_dict - 支援巢狀結構
實作步驟
步驟 1:設計 Field 基類
1from typing import Any, Optional, Type, Generic, TypeVar, Callable, get_type_hints
2
3T = TypeVar("T")
4
5class Field(Generic[T]):
6 """
7 Field Descriptor base class
8
9 Combines Django's field declaration style with Python's
10 descriptor protocol for type-safe, declarative model design.
11 """
12
13 def __init__(
14 self,
15 *,
16 default: Optional[T] = None,
17 required: bool = True,
18 serialized_name: Optional[str] = None,
19 validator: Optional[Callable[[T], bool]] = None,
20 error_msg: str = "Validation failed"
21 ):
22 """
23 Args:
24 default: Default value when not provided
25 required: Whether field is required (default True)
26 serialized_name: Name used in serialization (default: attribute name)
27 validator: Optional validation function
28 error_msg: Error message for validation failure
29 """
30 self.default = default
31 self.required = required
32 self.serialized_name = serialized_name
33 self.validator = validator
34 self.error_msg = error_msg
35
36 # Set by __set_name__
37 self.name: str = ""
38 self.private_name: str = ""
39
40 def __set_name__(self, owner: type, name: str) -> None:
41 """
42 Called automatically when the descriptor is assigned to a class attribute.
43
44 This is where we get the attribute name from the class definition,
45 eliminating the need to pass it explicitly.
46 """
47 self.name = name
48 self.private_name = f"_field_{name}"
49 # Use attribute name as serialized name if not specified
50 if self.serialized_name is None:
51 self.serialized_name = name
52
53 def __get__(self, obj: Any, objtype: type = None) -> Any:
54 """Return field value or descriptor itself if accessed on class."""
55 if obj is None:
56 return self # Class access returns descriptor
57 return getattr(obj, self.private_name, self.default)
58
59 def __set__(self, obj: Any, value: Any) -> None:
60 """Validate and set field value."""
61 # Handle None for optional fields
62 if value is None:
63 if self.required and self.default is None:
64 raise ValueError(f"{self.name}: This field is required")
65 setattr(obj, self.private_name, self.default)
66 return
67
68 # Run custom validator if provided
69 if self.validator and not self.validator(value):
70 raise ValueError(f"{self.name}: {self.error_msg}")
71
72 setattr(obj, self.private_name, value)
73
74 def serialize(self, value: T) -> Any:
75 """Convert Python value to serializable format."""
76 return value
77
78 def deserialize(self, value: Any) -> T:
79 """Convert serialized value to Python type."""
80 return value關鍵設計要點:
__set_name__自動取得屬性名稱,不需要像 Django 早期版本手動傳入serialized_name支援序列化時使用不同的欄位名稱(如 camelCase)serialize/deserialize方法供子類覆寫,實現型別轉換
步驟 2:實作型別特定的 Field
1import re
2from datetime import datetime
3from typing import List, Type, TypeVar
4
5class StringField(Field[str]):
6 """String field with optional pattern validation."""
7
8 def __init__(
9 self,
10 *,
11 pattern: Optional[str] = None,
12 min_length: int = 0,
13 max_length: Optional[int] = None,
14 **kwargs
15 ):
16 super().__init__(**kwargs)
17 self.pattern = re.compile(pattern) if pattern else None
18 self.min_length = min_length
19 self.max_length = max_length
20
21 def __set__(self, obj: Any, value: Any) -> None:
22 if value is not None:
23 if not isinstance(value, str):
24 raise TypeError(f"{self.name}: Expected str, got {type(value).__name__}")
25
26 if self.min_length and len(value) < self.min_length:
27 raise ValueError(f"{self.name}: Minimum length is {self.min_length}")
28
29 if self.max_length and len(value) > self.max_length:
30 raise ValueError(f"{self.name}: Maximum length is {self.max_length}")
31
32 if self.pattern and not self.pattern.match(value):
33 raise ValueError(f"{self.name}: Does not match pattern")
34
35 super().__set__(obj, value)
36
37class IntField(Field[int]):
38 """Integer field with range validation."""
39
40 def __init__(
41 self,
42 *,
43 min_value: Optional[int] = None,
44 max_value: Optional[int] = None,
45 **kwargs
46 ):
47 super().__init__(**kwargs)
48 self.min_value = min_value
49 self.max_value = max_value
50
51 def __set__(self, obj: Any, value: Any) -> None:
52 if value is not None:
53 if not isinstance(value, int) or isinstance(value, bool):
54 raise TypeError(f"{self.name}: Expected int, got {type(value).__name__}")
55
56 if self.min_value is not None and value < self.min_value:
57 raise ValueError(f"{self.name}: Minimum value is {self.min_value}")
58
59 if self.max_value is not None and value > self.max_value:
60 raise ValueError(f"{self.name}: Maximum value is {self.max_value}")
61
62 super().__set__(obj, value)
63
64class BoolField(Field[bool]):
65 """Boolean field."""
66
67 def __set__(self, obj: Any, value: Any) -> None:
68 if value is not None and not isinstance(value, bool):
69 raise TypeError(f"{self.name}: Expected bool, got {type(value).__name__}")
70 super().__set__(obj, value)
71
72class ChoiceField(Field[str]):
73 """Field with predefined choices."""
74
75 def __init__(self, *, choices: tuple[str, ...], **kwargs):
76 super().__init__(**kwargs)
77 self.choices = choices
78
79 def __set__(self, obj: Any, value: Any) -> None:
80 if value is not None and value not in self.choices:
81 choices_str = ", ".join(repr(c) for c in self.choices)
82 raise ValueError(f"{self.name}: Must be one of: {choices_str}")
83 super().__set__(obj, value)
84
85class ListField(Field[list]):
86 """List field with item type validation."""
87
88 def __init__(self, *, item_field: Field, **kwargs):
89 # ListField defaults to empty list, not required
90 kwargs.setdefault("default", [])
91 kwargs.setdefault("required", False)
92 super().__init__(**kwargs)
93 self.item_field = item_field
94
95 def __set__(self, obj: Any, value: Any) -> None:
96 if value is not None:
97 if not isinstance(value, list):
98 raise TypeError(f"{self.name}: Expected list, got {type(value).__name__}")
99 # Validate each item using item_field's validation logic
100 # (simplified - in production you'd want proper item validation)
101 super().__set__(obj, value if value is not None else [])
102
103 def serialize(self, value: list) -> list:
104 """Serialize list items."""
105 return [self.item_field.serialize(item) for item in value]
106
107 def deserialize(self, value: list) -> list:
108 """Deserialize list items."""
109 return [self.item_field.deserialize(item) for item in value]
110
111class DateTimeField(Field[datetime]):
112 """DateTime field with ISO format serialization."""
113
114 def __init__(self, *, format: str = "%Y-%m-%dT%H:%M:%S", **kwargs):
115 super().__init__(**kwargs)
116 self.format = format
117
118 def __set__(self, obj: Any, value: Any) -> None:
119 if value is not None and not isinstance(value, datetime):
120 raise TypeError(f"{self.name}: Expected datetime, got {type(value).__name__}")
121 super().__set__(obj, value)
122
123 def serialize(self, value: datetime) -> str:
124 """Convert datetime to ISO string."""
125 return value.strftime(self.format) if value else None
126
127 def deserialize(self, value: str) -> datetime:
128 """Parse ISO string to datetime."""
129 return datetime.strptime(value, self.format) if value else None步驟 3:用 Metaclass 處理欄位收集
1class ModelMeta(type):
2 """
3 Metaclass for Model classes.
4
5 Responsibilities:
6 1. Collect all Field descriptors from class definition
7 2. Store field metadata for serialization/deserialization
8 3. Generate __init__ signature from fields
9 """
10
11 def __new__(mcs, name: str, bases: tuple, namespace: dict):
12 # Collect fields from this class and parent classes
13 fields: dict[str, Field] = {}
14
15 # Inherit fields from parent Model classes
16 for base in bases:
17 if hasattr(base, "_fields"):
18 fields.update(base._fields)
19
20 # Collect fields from current class
21 for attr_name, attr_value in namespace.items():
22 if isinstance(attr_value, Field):
23 fields[attr_name] = attr_value
24
25 # Store fields metadata
26 namespace["_fields"] = fields
27
28 return super().__new__(mcs, name, bases, namespace)
29
30class Model(metaclass=ModelMeta):
31 """
32 Base class for declarative models.
33
34 Provides:
35 - Automatic __init__ from field definitions
36 - to_dict() for serialization
37 - from_dict() for deserialization
38 """
39
40 _fields: dict[str, Field] # Set by metaclass
41
42 def __init__(self, **kwargs):
43 """
44 Initialize model from keyword arguments.
45
46 All defined fields can be passed as keyword arguments.
47 Required fields must be provided unless they have defaults.
48 """
49 # Set each field value, triggering descriptor validation
50 for field_name, field in self._fields.items():
51 value = kwargs.get(field_name, field.default)
52 setattr(self, field_name, value)
53
54 # Check for unknown fields
55 unknown = set(kwargs.keys()) - set(self._fields.keys())
56 if unknown:
57 raise TypeError(f"Unknown fields: {', '.join(unknown)}")
58
59 def to_dict(self) -> dict:
60 """
61 Serialize model to dictionary.
62
63 Uses each field's serialized_name and serialize() method.
64 """
65 result = {}
66 for field_name, field in self._fields.items():
67 value = getattr(self, field_name)
68 if value is not None or field.required:
69 serialized_name = field.serialized_name or field_name
70 result[serialized_name] = field.serialize(value)
71 return result
72
73 @classmethod
74 def from_dict(cls, data: dict) -> "Model":
75 """
76 Deserialize dictionary to model instance.
77
78 Handles field name mapping and type conversion.
79 """
80 kwargs = {}
81
82 for field_name, field in cls._fields.items():
83 serialized_name = field.serialized_name or field_name
84
85 if serialized_name in data:
86 kwargs[field_name] = field.deserialize(data[serialized_name])
87 elif field_name in data:
88 # Fallback to field name if serialized name not found
89 kwargs[field_name] = field.deserialize(data[field_name])
90
91 return cls(**kwargs)
92
93 def __repr__(self) -> str:
94 """Generate readable representation."""
95 field_strs = []
96 for field_name in self._fields:
97 value = getattr(self, field_name)
98 field_strs.append(f"{field_name}={value!r}")
99 return f"{self.__class__.__name__}({', '.join(field_strs)})"
100
101 def __eq__(self, other) -> bool:
102 """Compare models by their field values."""
103 if not isinstance(other, self.__class__):
104 return False
105 for field_name in self._fields:
106 if getattr(self, field_name) != getattr(other, field_name):
107 return False
108 return True步驟 4:加入巢狀 Model 支援
1class EmbeddedField(Field):
2 """
3 Embedded model field for nested structures.
4
5 Allows nesting Model instances within other Models.
6 """
7
8 def __init__(self, *, model_class: Type[Model], **kwargs):
9 super().__init__(**kwargs)
10 self.model_class = model_class
11
12 def __set__(self, obj: Any, value: Any) -> None:
13 if value is not None:
14 # Accept dict and convert to model
15 if isinstance(value, dict):
16 value = self.model_class.from_dict(value)
17 elif not isinstance(value, self.model_class):
18 raise TypeError(
19 f"{self.name}: Expected {self.model_class.__name__} or dict, "
20 f"got {type(value).__name__}"
21 )
22 super().__set__(obj, value)
23
24 def serialize(self, value: Model) -> dict:
25 """Serialize embedded model to dict."""
26 return value.to_dict() if value else None
27
28 def deserialize(self, value: dict) -> Model:
29 """Deserialize dict to embedded model."""
30 return self.model_class.from_dict(value) if value else None完整程式碼
1#!/usr/bin/env python3
2"""
3Django-style Field Descriptor - Complete Implementation
4
5Demonstrates how to combine Descriptor protocol with Metaclass
6to create a declarative API similar to Django Model Fields.
7"""
8
9from __future__ import annotations
10import re
11from datetime import datetime
12from typing import Any, Optional, Type, Generic, TypeVar, Callable
13
14T = TypeVar("T")
15
16# ===== Field Descriptors =====
17
18class Field(Generic[T]):
19 """Base Field Descriptor with validation and serialization."""
20
21 def __init__(
22 self,
23 *,
24 default: Optional[T] = None,
25 required: bool = True,
26 serialized_name: Optional[str] = None,
27 validator: Optional[Callable[[T], bool]] = None,
28 error_msg: str = "Validation failed"
29 ):
30 self.default = default
31 self.required = required
32 self.serialized_name = serialized_name
33 self.validator = validator
34 self.error_msg = error_msg
35 self.name: str = ""
36 self.private_name: str = ""
37
38 def __set_name__(self, owner: type, name: str) -> None:
39 self.name = name
40 self.private_name = f"_field_{name}"
41 if self.serialized_name is None:
42 self.serialized_name = name
43
44 def __get__(self, obj: Any, objtype: type = None) -> Any:
45 if obj is None:
46 return self
47 return getattr(obj, self.private_name, self.default)
48
49 def __set__(self, obj: Any, value: Any) -> None:
50 if value is None:
51 if self.required and self.default is None:
52 raise ValueError(f"{self.name}: This field is required")
53 setattr(obj, self.private_name, self.default)
54 return
55
56 if self.validator and not self.validator(value):
57 raise ValueError(f"{self.name}: {self.error_msg}")
58
59 setattr(obj, self.private_name, value)
60
61 def serialize(self, value: T) -> Any:
62 return value
63
64 def deserialize(self, value: Any) -> T:
65 return value
66
67class StringField(Field[str]):
68 """String field with pattern and length validation."""
69
70 def __init__(
71 self,
72 *,
73 pattern: Optional[str] = None,
74 min_length: int = 0,
75 max_length: Optional[int] = None,
76 **kwargs
77 ):
78 super().__init__(**kwargs)
79 self.pattern = re.compile(pattern) if pattern else None
80 self.min_length = min_length
81 self.max_length = max_length
82
83 def __set__(self, obj: Any, value: Any) -> None:
84 if value is not None:
85 if not isinstance(value, str):
86 raise TypeError(f"{self.name}: Expected str, got {type(value).__name__}")
87 if self.min_length and len(value) < self.min_length:
88 raise ValueError(f"{self.name}: Minimum length is {self.min_length}")
89 if self.max_length and len(value) > self.max_length:
90 raise ValueError(f"{self.name}: Maximum length is {self.max_length}")
91 if self.pattern and not self.pattern.match(value):
92 raise ValueError(f"{self.name}: Does not match required pattern")
93 super().__set__(obj, value)
94
95class IntField(Field[int]):
96 """Integer field with range validation."""
97
98 def __init__(
99 self,
100 *,
101 min_value: Optional[int] = None,
102 max_value: Optional[int] = None,
103 **kwargs
104 ):
105 super().__init__(**kwargs)
106 self.min_value = min_value
107 self.max_value = max_value
108
109 def __set__(self, obj: Any, value: Any) -> None:
110 if value is not None:
111 if not isinstance(value, int) or isinstance(value, bool):
112 raise TypeError(f"{self.name}: Expected int, got {type(value).__name__}")
113 if self.min_value is not None and value < self.min_value:
114 raise ValueError(f"{self.name}: Minimum value is {self.min_value}")
115 if self.max_value is not None and value > self.max_value:
116 raise ValueError(f"{self.name}: Maximum value is {self.max_value}")
117 super().__set__(obj, value)
118
119class BoolField(Field[bool]):
120 """Boolean field."""
121
122 def __set__(self, obj: Any, value: Any) -> None:
123 if value is not None and not isinstance(value, bool):
124 raise TypeError(f"{self.name}: Expected bool, got {type(value).__name__}")
125 super().__set__(obj, value)
126
127class ChoiceField(Field[str]):
128 """Field with predefined choices."""
129
130 def __init__(self, *, choices: tuple[str, ...], **kwargs):
131 super().__init__(**kwargs)
132 self.choices = choices
133
134 def __set__(self, obj: Any, value: Any) -> None:
135 if value is not None and value not in self.choices:
136 choices_str = ", ".join(repr(c) for c in self.choices)
137 raise ValueError(f"{self.name}: Must be one of: {choices_str}")
138 super().__set__(obj, value)
139
140class ListField(Field[list]):
141 """List field with item type."""
142
143 def __init__(self, *, item_field: Field, **kwargs):
144 kwargs.setdefault("default", [])
145 kwargs.setdefault("required", False)
146 super().__init__(**kwargs)
147 self.item_field = item_field
148
149 def __set__(self, obj: Any, value: Any) -> None:
150 if value is not None and not isinstance(value, list):
151 raise TypeError(f"{self.name}: Expected list, got {type(value).__name__}")
152 super().__set__(obj, value if value is not None else [])
153
154 def serialize(self, value: list) -> list:
155 return [self.item_field.serialize(item) for item in (value or [])]
156
157 def deserialize(self, value: list) -> list:
158 return [self.item_field.deserialize(item) for item in (value or [])]
159
160class DateTimeField(Field[datetime]):
161 """DateTime field with ISO format."""
162
163 def __init__(self, *, format: str = "%Y-%m-%dT%H:%M:%S", **kwargs):
164 super().__init__(**kwargs)
165 self.format = format
166
167 def __set__(self, obj: Any, value: Any) -> None:
168 if value is not None and not isinstance(value, datetime):
169 raise TypeError(f"{self.name}: Expected datetime, got {type(value).__name__}")
170 super().__set__(obj, value)
171
172 def serialize(self, value: datetime) -> Optional[str]:
173 return value.strftime(self.format) if value else None
174
175 def deserialize(self, value: str) -> Optional[datetime]:
176 return datetime.strptime(value, self.format) if value else None
177
178# ===== Metaclass and Model =====
179
180class ModelMeta(type):
181 """Metaclass that collects Field descriptors."""
182
183 def __new__(mcs, name: str, bases: tuple, namespace: dict):
184 fields: dict[str, Field] = {}
185
186 # Inherit from parent classes
187 for base in bases:
188 if hasattr(base, "_fields"):
189 fields.update(base._fields)
190
191 # Collect from current class
192 for attr_name, attr_value in namespace.items():
193 if isinstance(attr_value, Field):
194 fields[attr_name] = attr_value
195
196 namespace["_fields"] = fields
197 return super().__new__(mcs, name, bases, namespace)
198
199class Model(metaclass=ModelMeta):
200 """Base Model with automatic serialization."""
201
202 _fields: dict[str, Field]
203
204 def __init__(self, **kwargs):
205 for field_name, field in self._fields.items():
206 value = kwargs.get(field_name, field.default)
207 setattr(self, field_name, value)
208
209 unknown = set(kwargs.keys()) - set(self._fields.keys())
210 if unknown:
211 raise TypeError(f"Unknown fields: {', '.join(unknown)}")
212
213 def to_dict(self) -> dict:
214 result = {}
215 for field_name, field in self._fields.items():
216 value = getattr(self, field_name)
217 if value is not None or field.required:
218 key = field.serialized_name or field_name
219 result[key] = field.serialize(value)
220 return result
221
222 @classmethod
223 def from_dict(cls, data: dict) -> "Model":
224 kwargs = {}
225 for field_name, field in cls._fields.items():
226 key = field.serialized_name or field_name
227 if key in data:
228 kwargs[field_name] = field.deserialize(data[key])
229 elif field_name in data:
230 kwargs[field_name] = field.deserialize(data[field_name])
231 return cls(**kwargs)
232
233 def __repr__(self) -> str:
234 parts = [f"{k}={getattr(self, k)!r}" for k in self._fields]
235 return f"{self.__class__.__name__}({', '.join(parts)})"
236
237 def __eq__(self, other) -> bool:
238 if not isinstance(other, self.__class__):
239 return False
240 return all(
241 getattr(self, k) == getattr(other, k)
242 for k in self._fields
243 )
244
245class EmbeddedField(Field):
246 """Embedded model field for nested structures."""
247
248 def __init__(self, *, model_class: Type[Model], **kwargs):
249 super().__init__(**kwargs)
250 self.model_class = model_class
251
252 def __set__(self, obj: Any, value: Any) -> None:
253 if value is not None:
254 if isinstance(value, dict):
255 value = self.model_class.from_dict(value)
256 elif not isinstance(value, self.model_class):
257 raise TypeError(
258 f"{self.name}: Expected {self.model_class.__name__} or dict"
259 )
260 super().__set__(obj, value)
261
262 def serialize(self, value: Model) -> Optional[dict]:
263 return value.to_dict() if value else None
264
265 def deserialize(self, value: dict) -> Optional[Model]:
266 return self.model_class.from_dict(value) if value else None使用範例
1# ===== Define Models =====
2
3class HookSpecificOutput(Model):
4 """Nested model for hook-specific output."""
5
6 hook_event_name = ChoiceField(
7 choices=("PreToolUse", "PostToolUse", "Stop"),
8 serialized_name="hookEventName"
9 )
10 permission_decision = ChoiceField(
11 choices=("allow", "deny", "ask"),
12 serialized_name="permissionDecision"
13 )
14 permission_reason = StringField(
15 min_length=1,
16 serialized_name="permissionDecisionReason"
17 )
18 user_prompt = StringField(
19 required=False,
20 serialized_name="userPrompt"
21 )
22
23class HookOutput(Model):
24 """
25 Hook Output model - similar to create_pretooluse_output
26
27 Declarative definition replaces the factory function.
28 """
29
30 hook_specific_output = EmbeddedField(
31 model_class=HookSpecificOutput,
32 serialized_name="hookSpecificOutput"
33 )
34 system_message = StringField(
35 required=False,
36 serialized_name="systemMessage"
37 )
38 suppress_output = BoolField(
39 default=False,
40 required=False,
41 serialized_name="suppressOutput"
42 )
43
44# ===== Usage Examples =====
45
46if __name__ == "__main__":
47 # Create model instance
48 output = HookOutput(
49 hook_specific_output=HookSpecificOutput(
50 hook_event_name="PreToolUse",
51 permission_decision="allow",
52 permission_reason="Operation permitted"
53 ),
54 system_message="Check completed"
55 )
56 print(f"Created: {output}")
57
58 # Serialize to dict (ready for JSON)
59 data = output.to_dict()
60 print(f"Serialized: {data}")
61 # Output:
62 # {
63 # "hookSpecificOutput": {
64 # "hookEventName": "PreToolUse",
65 # "permissionDecision": "allow",
66 # "permissionDecisionReason": "Operation permitted"
67 # },
68 # "systemMessage": "Check completed",
69 # "suppressOutput": False
70 # }
71
72 # Deserialize from dict
73 restored = HookOutput.from_dict(data)
74 print(f"Restored: {restored}")
75 print(f"Equal: {output == restored}")
76
77 # Validation examples
78 try:
79 bad = HookSpecificOutput(
80 hook_event_name="InvalidEvent", # Error: not in choices
81 permission_decision="allow",
82 permission_reason="Test"
83 )
84 except ValueError as e:
85 print(f"Validation error: {e}")
86
87 # Nested dict conversion
88 output2 = HookOutput(
89 hook_specific_output={ # Auto-converts dict to model
90 "hook_event_name": "PostToolUse",
91 "permission_decision": "deny",
92 "permission_reason": "Access denied"
93 }
94 )
95 print(f"From nested dict: {output2}")執行結果:
1Created: HookOutput(hook_specific_output=HookSpecificOutput(...), system_message='Check completed', suppress_output=False)
2Serialized: {'hookSpecificOutput': {'hookEventName': 'PreToolUse', 'permissionDecision': 'allow', 'permissionDecisionReason': 'Operation permitted'}, 'systemMessage': 'Check completed', 'suppressOutput': False}
3Restored: HookOutput(hook_specific_output=HookSpecificOutput(...), system_message='Check completed', suppress_output=False)
4Equal: True
5Validation error: hook_event_name: Must be one of: 'PreToolUse', 'PostToolUse', 'Stop'
6From nested dict: HookOutput(hook_specific_output=HookSpecificOutput(...), system_message=None, suppress_output=False)設計權衡
| 面向 | 函式工廠模式 | Field Descriptor |
|---|---|---|
| 學習曲線 | 低,只需了解函式 | 中,需理解 Descriptor + Metaclass |
| 程式碼量 | 每個輸出類型一個函式 | 一次性建立後可重用 |
| 欄位定義 | 分散在函式簽名和函式體 | 集中在類別定義中 |
| 型別提示 | 需手動維護 | 與欄位定義整合 |
| 序列化 | 每個函式獨立實作 | 自動生成 |
| 驗證時機 | 呼叫時驗證 | 賦值時驗證 |
| 巢狀結構 | 需手動處理 | EmbeddedField 自動處理 |
| Schema 生成 | 無法自動化 | 可從 _fields 元資料生成 |
| 調試 | 直覺,錯誤位置明確 | 需理解 Descriptor 協議 |
關鍵差異說明
函式工廠模式(如 hook_io.py):
- 適合簡單、一次性的資料建構
- 不需要額外的學習成本
- 但重複程式碼多,難以維護
Field Descriptor 模式:
- 初期投入較高,但長期收益大
- 欄位定義即文檔
- 易於擴展和測試
什麼時候該用這個技術?
適合使用:
- ORM/ODM 設計(如 Django Model、MongoDB ODM)
- 配置檔案解析(型別安全的 YAML/JSON 解析)
- API 請求/回應模型(REST API、GraphQL)
- 需要 Schema 自動生成的場景
- 多個類似結構的資料類別
不建議使用:
- 簡單的資料容器(直接用 dataclass)
- 不需要序列化的內部類別
- Pydantic 已經滿足需求
- 團隊不熟悉元編程
- 單一用途的資料結構
與 Pydantic 的比較
| 功能 | 自製 Field Descriptor | Pydantic |
|---|---|---|
| 型別驗證 | 手動實作 | 自動 |
| JSON Schema | 需額外實作 | 內建 |
| 效能 | 依實作而定 | 高度優化 |
| 學習價值 | 高,理解原理 | 低,直接使用 |
| 客製化彈性 | 完全控制 | 受框架限制 |
建議:在生產環境優先考慮 Pydantic;學習元編程時使用自製實作。
練習
基礎練習
1. 實作 BoolField 和 DateTimeField
完成以下 Field 類別,支援型別驗證和序列化:
1class BoolField(Field[bool]):
2 """Boolean field with strict type checking."""
3 pass # TODO: implement
4
5class DateTimeField(Field[datetime]):
6 """DateTime field with custom format."""
7 pass # TODO: implement2. 新增欄位的預設值和 required 屬性
修改 Field 基類,讓以下程式碼能正確運作:
1class Config(Model):
2 name = StringField(required=True)
3 timeout = IntField(default=30, required=False)
4 debug = BoolField(default=False, required=False)
5
6# 應該能建立實例
7config = Config(name="test")
8assert config.timeout == 30
9assert config.debug == False進階練習
3. 實作巢狀 Model(EmbeddedField)
讓以下程式碼能正確運作:
1class Address(Model):
2 city = StringField()
3 street = StringField()
4
5class Person(Model):
6 name = StringField()
7 address = EmbeddedField(model_class=Address)
8
9# 從巢狀 dict 建立
10person = Person.from_dict({
11 "name": "Alice",
12 "address": {"city": "Taipei", "street": "Main St"}
13})
14
15# 序列化回 dict
16data = person.to_dict()4. 實作 ListField 的完整驗證
讓以下程式碼能驗證列表中的每個項目:
1class Tag(Model):
2 name = StringField(pattern=r"^[a-z]+$")
3 priority = IntField(min_value=1, max_value=10)
4
5class Article(Model):
6 title = StringField()
7 tags = ListField(item_field=EmbeddedField(model_class=Tag))
8
9# 每個 tag 都應該被驗證
10article = Article(
11 title="Python Tips",
12 tags=[
13 {"name": "python", "priority": 5},
14 {"name": "INVALID", "priority": 5} # Should raise error
15 ]
16)挑戰題
5. 實作 Schema 自動生成
為 Model 類別新增 to_json_schema() 類別方法,自動生成 JSON Schema:
1class User(Model):
2 name = StringField(min_length=1, max_length=100)
3 age = IntField(min_value=0, max_value=150)
4 email = StringField(pattern=r"^[\w.-]+@[\w.-]+\.\w+$")
5
6schema = User.to_json_schema()
7# 應該輸出:
8# {
9# "type": "object",
10# "properties": {
11# "name": {"type": "string", "minLength": 1, "maxLength": 100},
12# "age": {"type": "integer", "minimum": 0, "maximum": 150},
13# "email": {"type": "string", "pattern": "^[\\w.-]+@[\\w.-]+\\.\\w+$"}
14# },
15# "required": ["name", "age", "email"]
16# }延伸閱讀
- Django Model Field 原始碼
- Pydantic Field 實作
- Python Descriptor HOWTO
- attrs 專案 - 另一個優秀的 Python 類別輔助工具
#python #python-advanced #metaprogramming #descriptor #case-study