案例:泛型驗證器
案例:泛型驗證器
1. 實作
2. 實作
1. 實作
2. 實作
1. 實作
本案例基於 .claude/lib/hook_validator.py 的實際程式碼,展示如何用 Generic 和 TypeVar 建立型別安全的通用驗證器。
先備知識
問題背景
現有設計
hook_validator.py 針對特定類型設計:
1from dataclasses import dataclass, field
2from pathlib import Path
3from typing import Optional, List
4
5@dataclass
6class ValidationIssue:
7 """Single validation issue"""
8 level: str # "error" | "warning" | "info"
9 message: str
10 line: Optional[int] = None
11 suggestion: Optional[str] = None
12
13@dataclass
14class ValidationResult:
15 """Validation result for a single hook"""
16 hook_path: str
17 issues: List[ValidationIssue] = field(default_factory=list)
18 is_compliant: bool = True
19
20 def __post_init__(self):
21 """Calculate is_compliant status"""
22 self.is_compliant = not any(
23 issue.level == "error" for issue in self.issues
24 )
25
26class HookValidator:
27 """Hook compliance validator - specific to Path type"""
28
29 def validate_hook(self, hook_path: str) -> ValidationResult:
30 """Validate a single hook file"""
31 hook_path = self._resolve_path(hook_path)
32
33 if not hook_path.exists():
34 return ValidationResult(
35 hook_path=str(hook_path),
36 issues=[ValidationIssue(
37 level="error",
38 message=f"Hook file not found: {hook_path}"
39 )]
40 )
41
42 # ... more validation logic ...
43 return ValidationResult(hook_path=str(hook_path))
44
45 def _resolve_path(self, path: str) -> Path:
46 """Resolve path to absolute path"""
47 p = Path(path)
48 return p if p.is_absolute() else Path.cwd() / p這個設計的優點
- 針對具體需求設計:專為驗證 Hook 檔案設計,邏輯清晰
- 型別明確:輸入是
str,輸出是ValidationResult
這個設計的限制
當需要驗證其他類型時:
- 需要複製大量相似程式碼:如果要驗證 API 回應、設定檔、表單輸入,需要寫多個類似的 Validator
- 驗證邏輯無法重用:「檢查是否為空」「檢查格式」這些通用邏輯無法跨 Validator 共享
- 型別檢查不夠通用:
ValidationResult綁定了hook_path: str,無法用於其他場景
進階解決方案
設計目標
- 建立通用的驗證器介面:定義
Validator[T]協議 - 支援任意輸入類型:可以驗證
Path、str、dict、自訂類別 - 保持型別安全:靜態型別檢查器能捕捉型別錯誤
- 支援驗證器組合:用
And、Or、Not組合基本驗證器
實作步驟
步驟 1:定義泛型 Validator 協議
首先,我們需要定義「什麼是驗證器」。使用 Protocol 和 TypeVar 來建立泛型介面:
1from typing import Protocol, TypeVar, Generic
2from dataclasses import dataclass, field
3from abc import abstractmethod
4
5# Define a type variable for the input type
6T = TypeVar("T")
7
8# Type variable with contravariance for Protocol
9T_contra = TypeVar("T_contra", contravariant=True)
10
11@dataclass
12class ValidationResult(Generic[T]):
13 """
14 Generic validation result
15
16 Type parameter T represents the validated value type.
17 This allows type-safe access to the validated value.
18 """
19 value: T
20 is_valid: bool = True
21 errors: list[str] = field(default_factory=list)
22 warnings: list[str] = field(default_factory=list)
23
24 def add_error(self, message: str) -> "ValidationResult[T]":
25 """Add an error and mark as invalid"""
26 self.errors.append(message)
27 self.is_valid = False
28 return self
29
30 def add_warning(self, message: str) -> "ValidationResult[T]":
31 """Add a warning (does not affect validity)"""
32 self.warnings.append(message)
33 return self
34
35class Validator(Protocol[T_contra]):
36 """
37 Generic validator protocol
38
39 Any class that implements validate(value: T) -> ValidationResult[T]
40 is considered a Validator[T].
41
42 Using contravariant type variable because validators consume values.
43 A Validator[Animal] can validate Dog (subtype), so it's contravariant.
44 """
45
46 def validate(self, value: T_contra) -> ValidationResult:
47 """Validate the given value and return the result"""
48 ...關鍵設計決策:
T_contra使用逆變(contravariant),因為驗證器是「消費者」ValidationResult[T]是泛型,讓結果可以攜帶原始值的型別資訊- Protocol 而非 ABC,支援結構型子型別(不需要顯式繼承)
步驟 2:實作具體驗證器
接下來實作幾個常用的基礎驗證器:
1from pathlib import Path
2import re
3
4class NotEmptyValidator:
5 """Validate that a string is not empty"""
6
7 def validate(self, value: str) -> ValidationResult[str]:
8 result = ValidationResult(value=value)
9 if not value or not value.strip():
10 result.add_error("Value cannot be empty")
11 return result
12
13class PathExistsValidator:
14 """Validate that a path exists"""
15
16 def __init__(self, must_be_file: bool = False, must_be_dir: bool = False):
17 self.must_be_file = must_be_file
18 self.must_be_dir = must_be_dir
19
20 def validate(self, value: Path) -> ValidationResult[Path]:
21 result = ValidationResult(value=value)
22
23 if not value.exists():
24 result.add_error(f"Path does not exist: {value}")
25 elif self.must_be_file and not value.is_file():
26 result.add_error(f"Path is not a file: {value}")
27 elif self.must_be_dir and not value.is_dir():
28 result.add_error(f"Path is not a directory: {value}")
29
30 return result
31
32class PatternValidator:
33 """Validate that a string matches a regex pattern"""
34
35 def __init__(self, pattern: str, error_message: str | None = None):
36 self.pattern = re.compile(pattern)
37 self.error_message = error_message or f"Value must match pattern: {pattern}"
38
39 def validate(self, value: str) -> ValidationResult[str]:
40 result = ValidationResult(value=value)
41 if not self.pattern.match(value):
42 result.add_error(self.error_message)
43 return result
44
45class RangeValidator:
46 """Validate that a number is within a range"""
47
48 def __init__(
49 self,
50 min_value: float | None = None,
51 max_value: float | None = None
52 ):
53 self.min_value = min_value
54 self.max_value = max_value
55
56 def validate(self, value: float | int) -> ValidationResult[float | int]:
57 result = ValidationResult(value=value)
58
59 if self.min_value is not None and value < self.min_value:
60 result.add_error(f"Value {value} is below minimum {self.min_value}")
61 if self.max_value is not None and value > self.max_value:
62 result.add_error(f"Value {value} is above maximum {self.max_value}")
63
64 return result步驟 3:驗證器組合(And、Or、Not)
驗證器的威力來自於組合。實作三個組合器:
1from typing import Sequence
2
3class AndValidator(Generic[T]):
4 """
5 Combine multiple validators with AND logic
6
7 All validators must pass for the result to be valid.
8 """
9
10 def __init__(self, validators: Sequence[Validator[T]]):
11 self.validators = validators
12
13 def validate(self, value: T) -> ValidationResult[T]:
14 result = ValidationResult(value=value)
15
16 for validator in self.validators:
17 sub_result = validator.validate(value)
18 result.errors.extend(sub_result.errors)
19 result.warnings.extend(sub_result.warnings)
20
21 result.is_valid = len(result.errors) == 0
22 return result
23
24class OrValidator(Generic[T]):
25 """
26 Combine multiple validators with OR logic
27
28 At least one validator must pass for the result to be valid.
29 """
30
31 def __init__(self, validators: Sequence[Validator[T]]):
32 self.validators = validators
33
34 def validate(self, value: T) -> ValidationResult[T]:
35 result = ValidationResult(value=value)
36 all_errors: list[str] = []
37
38 for validator in self.validators:
39 sub_result = validator.validate(value)
40 if sub_result.is_valid:
41 # At least one passed, return success
42 result.warnings.extend(sub_result.warnings)
43 return result
44 all_errors.extend(sub_result.errors)
45
46 # All failed
47 result.add_error(
48 f"None of the validators passed. Errors: {'; '.join(all_errors)}"
49 )
50 return result
51
52class NotValidator(Generic[T]):
53 """
54 Negate a validator
55
56 The result is valid if the inner validator fails.
57 """
58
59 def __init__(self, validator: Validator[T], error_message: str | None = None):
60 self.validator = validator
61 self.error_message = error_message or "Validation should have failed"
62
63 def validate(self, value: T) -> ValidationResult[T]:
64 result = ValidationResult(value=value)
65 sub_result = self.validator.validate(value)
66
67 if sub_result.is_valid:
68 result.add_error(self.error_message)
69 # If inner failed, outer succeeds
70 return result步驟 4:型別安全的建構器
為了讓組合更流暢,加入建構器模式:
1from typing import Callable
2
3class ValidatorBuilder(Generic[T]):
4 """
5 Fluent builder for composing validators
6
7 Provides a chainable API for building complex validators.
8 """
9
10 def __init__(self):
11 self._validators: list[Validator[T]] = []
12
13 def add(self, validator: Validator[T]) -> "ValidatorBuilder[T]":
14 """Add a validator to the chain"""
15 self._validators.append(validator)
16 return self
17
18 def add_if(
19 self,
20 condition: bool,
21 validator: Validator[T]
22 ) -> "ValidatorBuilder[T]":
23 """Conditionally add a validator"""
24 if condition:
25 self._validators.append(validator)
26 return self
27
28 def build(self) -> Validator[T]:
29 """Build the final AND-combined validator"""
30 if len(self._validators) == 1:
31 return self._validators[0]
32 return AndValidator(self._validators)
33
34 def build_or(self) -> Validator[T]:
35 """Build with OR logic instead of AND"""
36 if len(self._validators) == 1:
37 return self._validators[0]
38 return OrValidator(self._validators)
39
40def validator_for(type_hint: type[T]) -> ValidatorBuilder[T]:
41 """
42 Create a type-safe validator builder
43
44 Usage:
45 validator = (
46 validator_for(str)
47 .add(NotEmptyValidator())
48 .add(PatternValidator(r"^[a-z]+$"))
49 .build()
50 )
51 """
52 return ValidatorBuilder[T]()完整程式碼
1#!/usr/bin/env python3
2"""
3Generic Validator System
4
5A type-safe, composable validation framework using Generic and TypeVar.
6"""
7
8from __future__ import annotations
9from abc import abstractmethod
10from dataclasses import dataclass, field
11from pathlib import Path
12from typing import (
13 Callable,
14 Generic,
15 Protocol,
16 Sequence,
17 TypeVar,
18 runtime_checkable,
19)
20import re
21
22# ===== Type Variables =====
23
24T = TypeVar("T")
25T_contra = TypeVar("T_contra", contravariant=True)
26
27# ===== Core Types =====
28
29@dataclass
30class ValidationResult(Generic[T]):
31 """
32 Generic validation result
33
34 Attributes:
35 value: The validated value (preserves type information)
36 is_valid: Whether validation passed
37 errors: List of error messages (cause validation failure)
38 warnings: List of warning messages (informational only)
39 """
40 value: T
41 is_valid: bool = True
42 errors: list[str] = field(default_factory=list)
43 warnings: list[str] = field(default_factory=list)
44
45 def add_error(self, message: str) -> ValidationResult[T]:
46 """Add an error and mark as invalid"""
47 self.errors.append(message)
48 self.is_valid = False
49 return self
50
51 def add_warning(self, message: str) -> ValidationResult[T]:
52 """Add a warning (does not affect validity)"""
53 self.warnings.append(message)
54 return self
55
56 def __bool__(self) -> bool:
57 """Allow using result in boolean context"""
58 return self.is_valid
59
60@runtime_checkable
61class Validator(Protocol[T_contra]):
62 """
63 Generic validator protocol
64
65 Any class implementing validate(value) -> ValidationResult
66 satisfies this protocol.
67 """
68
69 def validate(self, value: T_contra) -> ValidationResult:
70 """Validate the given value"""
71 ...
72
73# ===== Basic Validators =====
74
75class NotEmptyValidator:
76 """Validate that a string is not empty"""
77
78 def validate(self, value: str) -> ValidationResult[str]:
79 result = ValidationResult(value=value)
80 if not value or not value.strip():
81 result.add_error("Value cannot be empty")
82 return result
83
84class PathExistsValidator:
85 """Validate that a path exists"""
86
87 def __init__(
88 self,
89 must_be_file: bool = False,
90 must_be_dir: bool = False
91 ):
92 self.must_be_file = must_be_file
93 self.must_be_dir = must_be_dir
94
95 def validate(self, value: Path) -> ValidationResult[Path]:
96 result = ValidationResult(value=value)
97
98 if not value.exists():
99 result.add_error(f"Path does not exist: {value}")
100 elif self.must_be_file and not value.is_file():
101 result.add_error(f"Path is not a file: {value}")
102 elif self.must_be_dir and not value.is_dir():
103 result.add_error(f"Path is not a directory: {value}")
104
105 return result
106
107class PatternValidator:
108 """Validate string matches a regex pattern"""
109
110 def __init__(self, pattern: str, error_message: str | None = None):
111 self.pattern = re.compile(pattern)
112 self.error_message = error_message or f"Must match pattern: {pattern}"
113
114 def validate(self, value: str) -> ValidationResult[str]:
115 result = ValidationResult(value=value)
116 if not self.pattern.match(value):
117 result.add_error(self.error_message)
118 return result
119
120class RangeValidator:
121 """Validate number is within range"""
122
123 def __init__(
124 self,
125 min_value: float | None = None,
126 max_value: float | None = None
127 ):
128 self.min_value = min_value
129 self.max_value = max_value
130
131 def validate(self, value: float | int) -> ValidationResult[float | int]:
132 result = ValidationResult(value=value)
133 if self.min_value is not None and value < self.min_value:
134 result.add_error(f"Value {value} < minimum {self.min_value}")
135 if self.max_value is not None and value > self.max_value:
136 result.add_error(f"Value {value} > maximum {self.max_value}")
137 return result
138
139class LengthValidator:
140 """Validate string length"""
141
142 def __init__(
143 self,
144 min_length: int | None = None,
145 max_length: int | None = None
146 ):
147 self.min_length = min_length
148 self.max_length = max_length
149
150 def validate(self, value: str) -> ValidationResult[str]:
151 result = ValidationResult(value=value)
152 length = len(value)
153
154 if self.min_length is not None and length < self.min_length:
155 result.add_error(f"Length {length} < minimum {self.min_length}")
156 if self.max_length is not None and length > self.max_length:
157 result.add_error(f"Length {length} > maximum {self.max_length}")
158
159 return result
160
161class TypeValidator(Generic[T]):
162 """Validate value is of expected type"""
163
164 def __init__(self, expected_type: type[T], type_name: str | None = None):
165 self.expected_type = expected_type
166 self.type_name = type_name or expected_type.__name__
167
168 def validate(self, value: object) -> ValidationResult[T]:
169 if isinstance(value, self.expected_type):
170 return ValidationResult(value=value) # type: ignore
171 else:
172 result = ValidationResult(value=value) # type: ignore
173 result.add_error(
174 f"Expected {self.type_name}, got {type(value).__name__}"
175 )
176 return result
177
178# ===== Composite Validators =====
179
180class AndValidator(Generic[T]):
181 """Combine validators with AND logic (all must pass)"""
182
183 def __init__(self, validators: Sequence[Validator[T]]):
184 self.validators = list(validators)
185
186 def validate(self, value: T) -> ValidationResult[T]:
187 result = ValidationResult(value=value)
188
189 for validator in self.validators:
190 sub_result = validator.validate(value)
191 result.errors.extend(sub_result.errors)
192 result.warnings.extend(sub_result.warnings)
193
194 result.is_valid = len(result.errors) == 0
195 return result
196
197class OrValidator(Generic[T]):
198 """Combine validators with OR logic (at least one must pass)"""
199
200 def __init__(self, validators: Sequence[Validator[T]]):
201 self.validators = list(validators)
202
203 def validate(self, value: T) -> ValidationResult[T]:
204 result = ValidationResult(value=value)
205 all_errors: list[str] = []
206
207 for validator in self.validators:
208 sub_result = validator.validate(value)
209 if sub_result.is_valid:
210 result.warnings.extend(sub_result.warnings)
211 return result
212 all_errors.extend(sub_result.errors)
213
214 result.add_error(f"No validator passed: {'; '.join(all_errors)}")
215 return result
216
217class NotValidator(Generic[T]):
218 """Negate a validator (passes if inner validator fails)"""
219
220 def __init__(self, validator: Validator[T], error_message: str | None = None):
221 self.validator = validator
222 self.error_message = error_message or "Validation should have failed"
223
224 def validate(self, value: T) -> ValidationResult[T]:
225 result = ValidationResult(value=value)
226 sub_result = self.validator.validate(value)
227
228 if sub_result.is_valid:
229 result.add_error(self.error_message)
230
231 return result
232
233# ===== Builder =====
234
235class ValidatorBuilder(Generic[T]):
236 """Fluent builder for composing validators"""
237
238 def __init__(self):
239 self._validators: list[Validator[T]] = []
240
241 def add(self, validator: Validator[T]) -> ValidatorBuilder[T]:
242 """Add a validator"""
243 self._validators.append(validator)
244 return self
245
246 def add_if(
247 self,
248 condition: bool,
249 validator: Validator[T]
250 ) -> ValidatorBuilder[T]:
251 """Conditionally add a validator"""
252 if condition:
253 self._validators.append(validator)
254 return self
255
256 def build(self) -> Validator[T]:
257 """Build AND-combined validator"""
258 if len(self._validators) == 0:
259 raise ValueError("No validators added")
260 if len(self._validators) == 1:
261 return self._validators[0]
262 return AndValidator(self._validators)
263
264 def build_or(self) -> Validator[T]:
265 """Build OR-combined validator"""
266 if len(self._validators) == 0:
267 raise ValueError("No validators added")
268 if len(self._validators) == 1:
269 return self._validators[0]
270 return OrValidator(self._validators)
271
272def validator_for(type_hint: type[T]) -> ValidatorBuilder[T]:
273 """Create a type-safe validator builder"""
274 return ValidatorBuilder[T]()
275
276# ===== List Validator =====
277
278class ListValidator(Generic[T]):
279 """Validate each element in a list"""
280
281 def __init__(
282 self,
283 element_validator: Validator[T],
284 min_length: int | None = None,
285 max_length: int | None = None
286 ):
287 self.element_validator = element_validator
288 self.min_length = min_length
289 self.max_length = max_length
290
291 def validate(self, value: list[T]) -> ValidationResult[list[T]]:
292 result = ValidationResult(value=value)
293
294 # Check list length
295 if self.min_length is not None and len(value) < self.min_length:
296 result.add_error(f"List length {len(value)} < minimum {self.min_length}")
297 if self.max_length is not None and len(value) > self.max_length:
298 result.add_error(f"List length {len(value)} > maximum {self.max_length}")
299
300 # Validate each element
301 for i, item in enumerate(value):
302 sub_result = self.element_validator.validate(item)
303 for error in sub_result.errors:
304 result.add_error(f"[{i}] {error}")
305 for warning in sub_result.warnings:
306 result.add_warning(f"[{i}] {warning}")
307
308 return result
309
310# ===== Demo =====
311
312if __name__ == "__main__":
313 print("=== Generic Validator Demo ===\n")
314
315 # Example 1: Basic validators
316 print("1. Basic Validators")
317 print("-" * 40)
318
319 not_empty = NotEmptyValidator()
320 print(f" NotEmpty(''): {not_empty.validate('')}")
321 print(f" NotEmpty('hello'): {not_empty.validate('hello')}")
322
323 # Example 2: Path validator
324 print("\n2. Path Validator")
325 print("-" * 40)
326
327 path_validator = PathExistsValidator(must_be_file=True)
328 result = path_validator.validate(Path("/etc/hosts"))
329 print(f" /etc/hosts: valid={result.is_valid}")
330
331 result = path_validator.validate(Path("/nonexistent"))
332 print(f" /nonexistent: valid={result.is_valid}, errors={result.errors}")
333
334 # Example 3: Composed validators
335 print("\n3. Composed Validators (AND)")
336 print("-" * 40)
337
338 username_validator = AndValidator[str]([
339 NotEmptyValidator(),
340 LengthValidator(min_length=3, max_length=20),
341 PatternValidator(r"^[a-z][a-z0-9_]*$", "Must be lowercase alphanumeric"),
342 ])
343
344 test_usernames = ["", "ab", "valid_user", "Invalid", "a" * 25]
345 for username in test_usernames:
346 result = username_validator.validate(username)
347 status = "PASS" if result.is_valid else "FAIL"
348 print(f" '{username}': {status}")
349 if not result.is_valid:
350 for error in result.errors:
351 print(f" - {error}")
352
353 # Example 4: Builder pattern
354 print("\n4. Builder Pattern")
355 print("-" * 40)
356
357 email_validator = (
358 validator_for(str)
359 .add(NotEmptyValidator())
360 .add(PatternValidator(
361 r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$",
362 "Invalid email format"
363 ))
364 .build()
365 )
366
367 test_emails = ["", "invalid", "test@example.com"]
368 for email in test_emails:
369 result = email_validator.validate(email)
370 status = "PASS" if result.is_valid else "FAIL"
371 print(f" '{email}': {status}")
372
373 # Example 5: List validator
374 print("\n5. List Validator")
375 print("-" * 40)
376
377 tags_validator = ListValidator(
378 element_validator=AndValidator[str]([
379 NotEmptyValidator(),
380 LengthValidator(max_length=20),
381 ]),
382 min_length=1,
383 max_length=5
384 )
385
386 test_tags = [
387 ["python", "typing"],
388 [],
389 ["valid", "", "also-valid"],
390 ]
391
392 for tags in test_tags:
393 result = tags_validator.validate(tags)
394 status = "PASS" if result.is_valid else "FAIL"
395 print(f" {tags}: {status}")
396 if not result.is_valid:
397 for error in result.errors:
398 print(f" - {error}")
399
400 print("\n=== Demo Complete ===")使用範例
基本使用
1from pathlib import Path
2
3# Create validators
4not_empty = NotEmptyValidator()
5path_exists = PathExistsValidator(must_be_file=True)
6
7# Validate string
8result = not_empty.validate("hello")
9print(result.is_valid) # True
10
11result = not_empty.validate("")
12print(result.is_valid) # False
13print(result.errors) # ["Value cannot be empty"]
14
15# Validate path
16result = path_exists.validate(Path("/etc/hosts"))
17print(result.is_valid) # True (on Unix systems)
18
19# Using ValidationResult in boolean context
20if not_empty.validate("test"):
21 print("Validation passed!")組合驗證
1# Username validator: non-empty, 3-20 chars, lowercase alphanumeric
2username_validator = AndValidator[str]([
3 NotEmptyValidator(),
4 LengthValidator(min_length=3, max_length=20),
5 PatternValidator(r"^[a-z][a-z0-9_]*$"),
6])
7
8# Test cases
9result = username_validator.validate("valid_user")
10print(result.is_valid) # True
11
12result = username_validator.validate("ab")
13print(result.is_valid) # False
14print(result.errors) # ["Length 2 < minimum 3"]
15
16# OR validation: accept either email or username format
17login_validator = OrValidator[str]([
18 PatternValidator(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"),
19 PatternValidator(r"^[a-z][a-z0-9_]{2,19}$"),
20])
21
22print(login_validator.validate("user@example.com").is_valid) # True
23print(login_validator.validate("valid_user").is_valid) # True
24print(login_validator.validate("X").is_valid) # False使用 Builder 模式
1# Fluent API for building validators
2password_validator = (
3 validator_for(str)
4 .add(NotEmptyValidator())
5 .add(LengthValidator(min_length=8, max_length=128))
6 .add(PatternValidator(r".*[A-Z].*", "Must contain uppercase"))
7 .add(PatternValidator(r".*[a-z].*", "Must contain lowercase"))
8 .add(PatternValidator(r".*[0-9].*", "Must contain digit"))
9 .build()
10)
11
12result = password_validator.validate("weakpass")
13print(result.errors)
14# ["Must contain uppercase", "Must contain digit"]
15
16result = password_validator.validate("Strong1Password")
17print(result.is_valid) # True驗證列表元素
1# Validate a list of tags
2tag_validator = NotEmptyValidator()
3tags_validator = ListValidator(
4 element_validator=tag_validator,
5 min_length=1,
6 max_length=10
7)
8
9result = tags_validator.validate(["python", "typing", "generic"])
10print(result.is_valid) # True
11
12result = tags_validator.validate(["valid", "", "also-valid"])
13print(result.is_valid) # False
14print(result.errors) # ["[1] Value cannot be empty"]設計權衡
| 面向 | 具體類型 | 泛型設計 |
|---|---|---|
| 重用性 | 低:每個類型需要獨立實作 | 高:一次實作,多處使用 |
| 型別推導 | 簡單:型別固定 | 需要技巧:需正確標註 TypeVar |
| 學習曲線 | 低:直覺易懂 | 中:需理解 Generic、Protocol |
| IDE 支援 | 完整:型別明確 | 需要正確標註才能獲得完整支援 |
| 執行效能 | 略佳:無泛型開銷 | 略差:有 Protocol 檢查開銷 |
| 錯誤訊息 | 清晰:直接指出問題 | 可能較模糊:泛型相關錯誤不易讀 |
何時選擇泛型設計?
選擇泛型設計當:
- 驗證邏輯會用於多種類型
- 需要組合多個驗證器
- 正在建立可重用的驗證函式庫
- 重視編譯時期的型別安全
選擇具體類型當:
- 只驗證單一特定類型
- 驗證邏輯非常簡單
- 團隊對泛型不熟悉
- 效能是關鍵考量
什麼時候該用這個技術?
適合使用:
- 需要驗證多種類型的函式庫
- 驗證邏輯需要組合重用
- 重視型別安全
- API 設計需要表達「這個驗證器接受 T 類型」
不建議使用:
- 只驗證單一類型
- 驗證邏輯很簡單(幾行 if-else 就能搞定)
- 團隊不熟悉泛型語法
- 程式碼不會被重用
練習
基礎練習
1. 實作 RangeValidator[int] 和 LengthValidator[str]
參考上面的 RangeValidator 實作,確保它可以正確驗證整數範圍。
測試案例:
1age_validator = RangeValidator(min_value=0, max_value=150)
2assert age_validator.validate(25).is_valid
3assert not age_validator.validate(-1).is_valid
4assert not age_validator.validate(200).is_valid2. 實作 EmailValidator
建立一個 Email 驗證器,組合 NotEmptyValidator 和 PatternValidator:
1email_validator = EmailValidator()
2assert email_validator.validate("user@example.com").is_valid
3assert not email_validator.validate("invalid").is_valid進階練習
1. 實作 ListValidator[T] 驗證列表中的每個元素
建立一個泛型列表驗證器,可以:
- 驗證列表長度
- 對每個元素執行子驗證器
- 收集所有錯誤,標註元素索引
1int_list_validator = ListValidator(
2 element_validator=RangeValidator(min_value=0),
3 min_length=1
4)
5result = int_list_validator.validate([1, 2, -3, 4])
6# errors: ["[2] Value -3 < minimum 0"]2. 實作 ConditionalValidator[T]
建立一個條件驗證器,只在條件成立時執行驗證:
1# Only validate age if it's provided (not None)
2optional_age_validator = ConditionalValidator(
3 condition=lambda x: x is not None,
4 validator=RangeValidator(min_value=0, max_value=150)
5)挑戰題
1. 實作 SchemaValidator 驗證字典結構
建立一個驗證器,可以驗證字典的結構和值:
1user_schema = SchemaValidator({
2 "name": NotEmptyValidator(),
3 "age": RangeValidator(min_value=0, max_value=150),
4 "email": EmailValidator(),
5}, required_keys=["name", "email"])
6
7result = user_schema.validate({
8 "name": "Alice",
9 "age": 30,
10 "email": "alice@example.com"
11})
12assert result.is_valid
13
14result = user_schema.validate({
15 "name": "",
16 "age": -5,
17})
18# errors: ["name: Value cannot be empty", "age: Value -5 < minimum 0", "Missing required key: email"]提示:
- 使用
dict[str, Validator[Any]]作為 schema 類型 - 處理可選欄位和必填欄位
- 考慮巢狀 schema 的支援
延伸閱讀
上一章:異常設計架構 返回:模組 3.5:進階設計模式
#python #python-advanced #design-patterns #generics #case-study