第 4 卷:面向对象、数据建模与类型系统
学习方式:严格按顺序阅读。每个代码块只使用当前单元或此前单元已经讲过的语法。
本卷项目线:
taskflow/milestones/v04_modeled_tasks.py --demo完成本卷后:进入第 5 卷。
本卷说明
本卷把字典和松散函数升级为有明确不变量的对象、协议和类型别名。你会先学类如何工作,再学注解如何表达接口;类型检查通过仍不等于运行时校验。
本卷知识单元
- [4.1 类、实例与方法](#4.1 类、实例与方法 "#41-%E7%B1%BB%E5%AE%9E%E4%BE%8B%E4%B8%8E%E6%96%B9%E6%B3%95")
- [4.2 类型注解与函数接口](#4.2 类型注解与函数接口 "#42-%E7%B1%BB%E5%9E%8B%E6%B3%A8%E8%A7%A3%E4%B8%8E%E5%87%BD%E6%95%B0%E6%8E%A5%E5%8F%A3")
- [4.3 继承、MRO 与 super](#4.3 继承、MRO 与 super "#43-%E7%BB%A7%E6%89%BFmro-%E4%B8%8E-super")
- [4.4 魔术方法与对象协议](#4.4 魔术方法与对象协议 "#44-%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95%E4%B8%8E%E5%AF%B9%E8%B1%A1%E5%8D%8F%E8%AE%AE")
- [4.5 dataclass、不可变对象与默认工厂](#4.5 dataclass、不可变对象与默认工厂 "#45-dataclass%E4%B8%8D%E5%8F%AF%E5%8F%98%E5%AF%B9%E8%B1%A1%E4%B8%8E%E9%BB%98%E8%AE%A4%E5%B7%A5%E5%8E%82")
- [4.6 property、描述符与属性校验](#4.6 property、描述符与属性校验 "#46-property%E6%8F%8F%E8%BF%B0%E7%AC%A6%E4%B8%8E%E5%B1%9E%E6%80%A7%E6%A0%A1%E9%AA%8C")
- [4.7 ABC、Protocol 与组合](#4.7 ABC、Protocol 与组合 "#47-abcprotocol-%E4%B8%8E%E7%BB%84%E5%90%88")
- [4.8 枚举、常量与状态建模](#4.8 枚举、常量与状态建模 "#48-%E6%9E%9A%E4%B8%BE%E5%B8%B8%E9%87%8F%E4%B8%8E%E7%8A%B6%E6%80%81%E5%BB%BA%E6%A8%A1")
- [4.9 泛型、TypedDict、Literal 与静态检查](#4.9 泛型、TypedDict、Literal 与静态检查 "#49-%E6%B3%9B%E5%9E%8Btypeddictliteral-%E4%B8%8E%E9%9D%99%E6%80%81%E6%A3%80%E6%9F%A5")
- [4.10 复制、相等、哈希与 slots](#4.10 复制、相等、哈希与 slots "#410-%E5%A4%8D%E5%88%B6%E7%9B%B8%E7%AD%89%E5%93%88%E5%B8%8C%E4%B8%8E-slots")
- [4.11 OOP 与类型项目线:TaskFlow v04](#4.11 OOP 与类型项目线:TaskFlow v04 "#411-oop-%E4%B8%8E%E7%B1%BB%E5%9E%8B%E9%A1%B9%E7%9B%AE%E7%BA%BFtaskflow-v04")
- [4.12 dataclass 进阶:校验、字段策略与继承](#4.12 dataclass 进阶:校验、字段策略与继承 "#412-dataclass-%E8%BF%9B%E9%98%B6%E6%A0%A1%E9%AA%8C%E5%AD%97%E6%AE%B5%E7%AD%96%E7%95%A5%E4%B8%8E%E7%BB%A7%E6%89%BF")
- [4.13 对象协议进阶:可调用、索引与上下文](#4.13 对象协议进阶:可调用、索引与上下文 "#413-%E5%AF%B9%E8%B1%A1%E5%8D%8F%E8%AE%AE%E8%BF%9B%E9%98%B6%E5%8F%AF%E8%B0%83%E7%94%A8%E7%B4%A2%E5%BC%95%E4%B8%8E%E4%B8%8A%E4%B8%8B%E6%96%87")
- [4.14 typing 进阶:Callable、TypeVar 与 overload](#4.14 typing 进阶:Callable、TypeVar 与 overload "#414-typing-%E8%BF%9B%E9%98%B6callabletypevar-%E4%B8%8E-overload")
- [4.15 泛型接口与依赖注入组合](#4.15 泛型接口与依赖注入组合 "#415-%E6%B3%9B%E5%9E%8B%E6%8E%A5%E5%8F%A3%E4%B8%8E%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5%E7%BB%84%E5%90%88")
4.1 类、实例与方法
前置知识:第 1~3 卷;尤其是函数、异常和文件。
概念 :类把状态和行为组织在一起。实例属性属于每个对象,类属性由所有实例共享;实例方法显式接收 self,__init__ 负责初始化。Python 没有真正的私有字段,双下划线只会触发名称改写。
本单元新语法
| 语法形式 | 含义 |
|---|---|
class Name: |
定义类 |
def __init__(self, ...): |
初始化实例 |
self.attr = value |
设置实例属性 |
@classmethod |
接收类而不是实例 |
@staticmethod |
不接收 self/cls 的普通方法 |
最小示例
最小示例 1:任务对象
python
# 示例目标:任务对象
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:完整可运行
class Task:
def __init__(self, title):
self.title = title
self.done = False
def complete(self):
self.done = True
task = Task("Read")
task.complete()
print(task.title, task.done)
# 输出: Read True
# 示例类型:最小示例
最小示例 2:类方法构造实例
python
# 示例目标:类方法构造实例
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:完整可运行
class Task:
def __init__(self, title):
self.title = title
@classmethod
def empty(cls):
return cls("untitled")
print(Task.empty().title)
# 输出: untitled
# 示例类型:最小示例
实际场景示例
任务、用户、订单等有身份和行为的领域对象适合类;仅承载字段的内部数据优先用 dataclass。
python
# 示例目标:带校验的任务对象
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行
class Task:
def __init__(self, title):
cleaned = title.strip()
if not cleaned:
raise ValueError("title required")
self.title = cleaned
task = Task(" Read ")
print(task.title)
# 输出: Read
# 示例类型:实际场景
关联/组合
组合 1:静态方法
python
# 示例目标:静态方法
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行
class Task:
@staticmethod
def normalize(title):
return title.strip().lower()
print(Task.normalize(" Read "))
# 输出: read
# 示例类型:关联/组合
组合 2:实例属性独立
python
# 示例目标:实例属性独立
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行
class Task:
tags = []
def __init__(self):
self.items = []
a = Task()
b = Task()
a.items.append("x")
print(a.items, b.items)
# 输出: ['x'] []
# 示例类型:关联/组合
易错/易混
易错 1:用可变类属性保存实例状态
错误代码:
python
# 示例目标:用可变类属性保存实例状态(错误示范)
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行
class Task:
tags = []
a = Task()
b = Task()
a.tags.append("work")
print(b.tags)
# 示例类型:错误示范
实际结果:输出 'work';两个实例共享同一个类属性列表。
修正代码:
python
# 示例目标:用可变类属性保存实例状态(修正)
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行
class Task:
def __init__(self):
self.tags = []
a = Task()
b = Task()
a.tags.append("work")
print(b.tags)
# 输出: []
# 示例类型:易错修正
原因 :每个实例的状态在 init 中创建。
易错 2:忘记 self
错误代码:
python
# 示例目标:忘记 self(错误示范)
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行
class Task:
def show():
print("task")
Task().show()
# 示例类型:错误示范
实际结果:抛出 TypeError:实例被当作参数传入但方法不接收。
修正代码:
python
# 示例目标:忘记 self(修正)
# 前置知识:第 1~3 卷;尤其是函数、异常和文件。
# 代码状态:片段/按前置知识运行
class Task:
def show(self):
print("task")
Task().show()
# 输出: task
# 示例类型:易错修正
原因:实例方法至少接收 self。
练习
- 写一个
Task类,包含 title 和 done,并实现 complete 方法。 - 写一个类方法
empty()创建标题为 untitled 的实例。
4.2 类型注解与函数接口
前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
概念 :注解描述期望类型,主要服务阅读、IDE 和静态检查;运行时默认不会自动校验。内置泛型如 list[str] 从 3.9 起可用,X | None 从 3.10 起可用。
本单元新语法
| 语法形式 | 含义 |
|-------------------------|--------------|-----------|
| x: int | 变量/属性注解 |
| def f(x: int) -> str: | 参数与返回值注解 |
| list[str] | 元素为字符串的列表 |
| `str | None` | 字符串或 None |
| Optional[str] | 旧的等价写法 |
| reveal_type(x) | 让类型检查器显示推断类型 |
最小示例
最小示例 1:函数注解
python
# 示例目标:函数注解
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:完整可运行
def title_length(title: str) -> int:
return len(title)
print(title_length("read"))
# 输出: 4
# 示例类型:最小示例
最小示例 2:可选返回值
python
# 示例目标:可选返回值
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:完整可运行
def find(tasks: list[str], target: str) -> str | None:
return target if target in tasks else None
print(find(["read"], "read"))
print(find([], "read"))
# 输出: read
# 输出: None
# 示例类型:最小示例
实际场景示例
公共函数、服务接口和 DTO 使用注解;外部 JSON 仍要通过 Pydantic 或显式校验后才能进入有类型的世界。
python
# 示例目标:带注解的任务创建函数
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行
def create_task(title: str, tags: list[str]) -> dict[str, object]:
return {"title": title.strip(), "tags": tags}
print(create_task(" Read ", ["study"]))
# 输出: {'title': 'Read', 'tags': ['study']}
# 示例类型:实际场景
关联/组合
组合 1:容器注解
python
# 示例目标:容器注解
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行
def average(values: list[float]) -> float:
return sum(values) / len(values)
print(average([1.0, 3.0]))
# 输出: 2.0
# 示例类型:关联/组合
易错/易混
易错 1:把注解当运行时校验
错误代码:
python
# 示例目标:把注解当运行时校验(错误示范)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行
def create(age: int):
return age
print(create("18"))
# 示例类型:错误示范
实际结果:输出 18 字符串,注解不会拒绝它。
修正代码:
python
# 示例目标:把注解当运行时校验(修正)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行
def create(age: int):
if not isinstance(age, int):
raise TypeError("age must be int")
return age
try:
create("18")
except TypeError as exc:
print(exc)
# 输出: age must be int
# 示例类型:易错修正
原因:运行时输入需要显式校验。
易错 2:用 None 表示所有缺失
错误代码:
python
# 示例目标:用 None 表示所有缺失(错误示范)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行
def greet(name: str | None) -> str:
return "hi " + name
# 示例类型:错误示范
实际结果:name 为 None 时拼接失败。
修正代码:
python
# 示例目标:用 None 表示所有缺失(修正)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行
def greet(name: str | None) -> str:
if name is None:
name = "guest"
return "hi " + name
print(greet(None))
# 输出: hi guest
# 示例类型:易错修正
原因:可选类型必须在逻辑中处理 None 分支。
易错 3:注解不阻止运行时错误
错误代码:
python
# 示例目标:注解不阻止运行时错误(错误示范)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行
def length(value: str) -> int:
return len(value)
print(length(123))
# 示例类型:错误示范
实际结果:运行时抛出 TypeError;注解没有做转换或校验。
修正代码:
python
# 示例目标:注解不阻止运行时错误(修正)
# 前置知识:4.1 类;第 2 卷函数;第 3 卷异常。
# 代码状态:片段/按前置知识运行
def length(value: str) -> int:
if not isinstance(value, str):
raise TypeError("value must be str")
return len(value)
print(length(123))
# 示例类型:易错修正
原因:边界需要显式校验,注解只是静态契约。
练习
- 给
double(value)加参数和返回值注解。 - 写一个返回
str | None的查找函数。
4.3 继承、MRO 与 super
前置知识:4.1 类;4.2 注解;3.7 异常。
概念 :子类继承父类的属性和方法,方法查找沿 C3 线性化的 MRO 进行。super() 调用 MRO 中的下一个实现,适合协作式继承;跨模块复用优先组合,避免深继承。
本单元新语法
| 语法形式 | 含义 |
|---|---|
class Child(Parent): |
继承 |
super().__init__() |
调用 MRO 下一个初始化 |
isinstance(obj, Parent) |
检查类型关系 |
MRO |
方法解析顺序 |
mixin |
提供局部能力的辅助类 |
最小示例
最小示例 1:子类扩展父类
python
# 示例目标:子类扩展父类
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:完整可运行
class BaseTask:
def describe(self) -> str:
return "task"
class UrgentTask(BaseTask):
def describe(self) -> str:
return "urgent " + super().describe()
print(UrgentTask().describe())
# 输出: urgent task
# 示例类型:最小示例
最小示例 2:isinstance
python
# 示例目标:isinstance
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:完整可运行
class BaseTask:
pass
class UrgentTask(BaseTask):
pass
print(isinstance(UrgentTask(), BaseTask))
# 输出: True
# 示例类型:最小示例
实际场景示例
框架扩展点、领域对象层次和测试替身可能使用继承;如果只是需要复用行为,优先把对象作为属性组合进来。
python
# 示例目标:抽象任务接口的早期形态
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
class Task:
def run(self) -> str:
raise NotImplementedError
class ReadTask(Task):
def run(self) -> str:
return "read"
print(ReadTask().run())
# 输出: read
# 示例类型:实际场景
关联/组合
组合 1:调用父类初始化
python
# 示例目标:调用父类初始化
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
class Base:
def __init__(self, title: str) -> None:
self.title = title
class Child(Base):
def __init__(self, title: str, priority: str) -> None:
super().__init__(title)
self.priority = priority
child = Child("Read", "high")
print(child.title, child.priority)
# 输出: Read high
# 示例类型:关联/组合
组合 2:MRO 查看
python
# 示例目标:MRO 查看
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
class A:
pass
class B(A):
pass
print([cls.__name__ for cls in B.__mro__])
# 输出: ['B', 'A', 'object']
# 示例类型:关联/组合
易错/易混
易错 1:用继承代替组合
错误代码:
python
# 示例目标:用继承代替组合(错误示范)
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
class Task:
def save(self) -> None:
pass
class CachedTask(Task):
def get(self, key: str) -> str:
return key
# 示例类型:错误示范
实际结果:CachedTask 被迫继承无关的 save,类型层次变混乱。
修正代码:
python
# 示例目标:用继承代替组合(修正)
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
class Cache:
def get(self, key: str) -> str:
return key
class Task:
def __init__(self, cache: Cache) -> None:
self.cache = cache
print(Task(Cache()).cache.get("k"))
# 输出: k
# 示例类型:易错修正
原因:能力通过组合注入,避免把无关行为塞进继承树。
易错 2:省略 super 初始化
错误代码:
python
# 示例目标:省略 super 初始化(错误示范)
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
class Base:
def __init__(self) -> None:
self.ready = True
class Child(Base):
def __init__(self) -> None:
pass
print(Child().ready)
# 示例类型:错误示范
实际结果:抛出 AttributeError:父类初始化没有执行。
修正代码:
python
# 示例目标:省略 super 初始化(修正)
# 前置知识:4.1 类;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
class Base:
def __init__(self) -> None:
self.ready = True
class Child(Base):
def __init__(self) -> None:
super().__init__()
print(Child().ready)
# 输出: True
# 示例类型:易错修正
原因:子类覆盖初始化时显式调用协作式 super。
练习
- 写一个父类和子类,子类在 describe 中调用 super。
- 打印一个类的 MRO 名称。
4.4 魔术方法与对象协议
前置知识:4.1 类;4.3 继承。
概念 :Python 的内置语法通过魔术方法接入对象:len 调用 __len__,== 调用 __eq__,print 优先使用 __str__,调试使用 __repr__,with 使用 __enter__/__exit__。
本单元新语法
| 语法形式 | 含义 |
|---|---|
__repr__(self) |
开发者可读表示 |
__str__(self) |
用户可读表示 |
__eq__(self, other) |
相等性 |
__len__(self) |
长度 |
__iter__(self) |
迭代协议 |
__contains__(self, item) |
成员测试协议 |
__hash__(self) |
哈希协议 |
最小示例
最小示例 1:repr 与 eq
python
# 示例目标:repr 与 eq
# 前置知识:4.1 类;4.3 继承。
# 代码状态:完整可运行
class Task:
def __init__(self, title: str) -> None:
self.title = title
def __repr__(self) -> str:
return f"Task({self.title!r})"
def __eq__(self, other: object) -> bool:
return isinstance(other, Task) and self.title == other.title
print(Task("Read"))
print(Task("Read") == Task("Read"))
# 输出: Task('Read')
# 输出: True
# 示例类型:最小示例
最小示例 2:len 与 contains
python
# 示例目标:len 与 contains
# 前置知识:4.1 类;4.3 继承。
# 代码状态:完整可运行
class Tags:
def __init__(self, items: list[str]) -> None:
self.items = items
def __len__(self) -> int:
return len(self.items)
def __contains__(self, item: str) -> bool:
return item in self.items
tags = Tags(["work"])
print(len(tags), "work" in tags)
# 输出: 1 True
# 示例类型:最小示例
实际场景示例
值对象需要可读 repr 和明确相等性,容器需要 len/iter,上下文管理器需要 enter/exit。
python
# 示例目标:可哈希的值对象
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行
class Point:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def __eq__(self, other: object) -> bool:
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
def __hash__(self) -> int:
return hash((self.x, self.y))
print({Point(1, 2), Point(1, 2)})
# 输出包含: Point object 或自定义表示
# 示例类型:实际场景
关联/组合
组合 1:可迭代对象
python
# 示例目标:可迭代对象
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行
class Countdown:
def __init__(self, n: int) -> None:
self.n = n
def __iter__(self):
for value in range(self.n, 0, -1):
yield value
print(list(Countdown(3)))
# 输出: [3, 2, 1]
# 示例类型:关联/组合
组合 2:调试表示
python
# 示例目标:调试表示
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行
class Task:
def __repr__(self) -> str:
return "Task()"
print([Task()])
# 输出: [Task()]
# 示例类型:关联/组合
易错/易混
易错 1:实现 eq 却忽略 hash
错误代码:
python
# 示例目标:实现 eq 却忽略 hash(错误示范)
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行
class Task:
def __eq__(self, other: object) -> bool:
return True
task = Task()
print(hash(task))
# 示例类型:错误示范
实际结果:抛出 TypeError:unhashable type。
修正代码:
python
# 示例目标:实现 eq 却忽略 hash(修正)
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行
class Task:
def __eq__(self, other: object) -> bool:
return isinstance(other, Task)
def __hash__(self) -> int:
return 1
print(hash(Task()))
# 输出: 1
# 示例类型:易错修正
原因 :定义 eq 后要明确对象是否可哈希;值对象通常同时实现 hash。
易错 2:repr 返回非字符串
错误代码:
python
# 示例目标:repr 返回非字符串(错误示范)
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行
class Task:
def __repr__(self) -> str:
return 1
print(Task())
# 示例类型:错误示范
实际结果 :抛出 TypeError:repr returned non-string。
修正代码:
python
# 示例目标:repr 返回非字符串(修正)
# 前置知识:4.1 类;4.3 继承。
# 代码状态:片段/按前置知识运行
class Task:
def __repr__(self) -> str:
return "Task()"
print(Task())
# 输出: Task()
# 示例类型:易错修正
原因:魔术方法必须满足协议返回类型。
练习
- 让 Task 的
==按 title 比较。 - 实现
__len__,让len(Tags(["a"]))返回 1。
4.5 dataclass、不可变对象与默认工厂
前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
概念 :@dataclass 根据字段自动生成初始化、repr 和相等性。frozen=True 防止重新绑定,slots=True 减少实例字典开销;可变字段使用 default_factory,默认值不能直接写列表。
本单元新语法
| 语法形式 | 含义 |
|---|---|
@dataclass |
自动生成数据类方法 |
field(default_factory=list) |
为每个实例创建默认容器 |
frozen=True |
禁止重新绑定字段 |
slots=True |
限制实例属性并节省内存 |
replace(obj, ...) |
生成修改后的新对象 |
最小示例
最小示例 1:任务数据类
python
# 示例目标:任务数据类
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:完整可运行
from dataclasses import dataclass, field
@dataclass
class Task:
id: str
title: str
tags: list[str] = field(default_factory=list)
task = Task("a", "Read")
task.tags.append("study")
print(task)
# 输出包含: Task(id='a', title='Read', tags=['study'])
# 示例类型:最小示例
最小示例 2:不可变点
python
# 示例目标:不可变点
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:完整可运行
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
point = Point(1, 2)
print(point == Point(1, 2))
# 输出: True
# 示例类型:最小示例
实际场景示例
DTO、配置快照、值对象和测试数据适合 dataclass;外部不可信输入仍需运行时校验。
python
# 示例目标:修改生成新任务
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Task:
id: str
title: str
done: bool = False
original = Task("a", "Read")
completed = replace(original, done=True)
print(original.done, completed.done)
# 输出: False True
# 示例类型:实际场景
关联/组合
组合 1:列表默认工厂
python
# 示例目标:列表默认工厂
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass, field
@dataclass
class Box:
items: list[str] = field(default_factory=list)
a = Box()
b = Box()
a.items.append("x")
print(a.items, b.items)
# 输出: ['x'] []
# 示例类型:关联/组合
组合 2:slots
python
# 示例目标:slots
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass
@dataclass(slots=True)
class Task:
title: str
print(Task("Read").title)
# 输出: Read
# 示例类型:关联/组合
易错/易混
易错 1:直接把列表作为默认值
错误代码:
python
# 示例目标:直接把列表作为默认值(错误示范)
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass
@dataclass
class Task:
tags: list[str] = []
# 示例类型:错误示范
实际结果:当前 Python 会直接拒绝这个定义,因为可变默认值不安全。
修正代码:
python
# 示例目标:直接把列表作为默认值(修正)
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass, field
@dataclass
class Task:
tags: list[str] = field(default_factory=list)
print(Task().tags)
# 输出: []
# 示例类型:易错修正
原因:可变默认值必须用 default_factory 为每个实例创建新对象。
易错 2:frozen 不等于深不可变
错误代码:
python
# 示例目标:frozen 不等于深不可变(错误示范)
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass
@dataclass(frozen=True)
class Task:
tags: list[str]
task = Task(["work"])
task.tags.append("home")
print(task.tags)
# 示例类型:错误示范
实际结果:输出 'work', 'home';frozen 只阻止字段重新绑定。
修正代码:
python
# 示例目标:frozen 不等于深不可变(修正)
# 前置知识:4.1 类;4.4 魔术方法;3.2 装饰器。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass
@dataclass(frozen=True)
class Task:
tags: tuple[str, ...]
task = Task(("work",))
print(task.tags)
# 输出: ('work',)
# 示例类型:易错修正
原因:需要深层不可变时使用元组等不可变字段。
练习
- 定义包含 id、title、tags 的 dataclass,tags 使用默认工厂。
- 用
replace从 frozen Task 生成 done=True 的新对象。
4.6 property、描述符与属性校验
前置知识:4.1 类;4.4 魔术方法;3.7 异常。
概念:property 把方法包装成属性访问,适合校验和派生值。描述符是更底层的属性协议,property 本身就是描述符;ORM 字段和框架配置常使用描述符。
本单元新语法
| 语法形式 | 含义 |
|---|---|
@property |
只读计算属性 |
@name.setter |
属性赋值校验 |
cached_property |
首次计算后缓存 |
__get__/__set__ |
描述符协议 |
__set_name__ |
描述符获知字段名 |
最小示例
最小示例 1:只读派生属性
python
# 示例目标:只读派生属性
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:完整可运行
class Task:
def __init__(self, title: str) -> None:
self.title = title
@property
def slug(self) -> str:
return self.title.lower().replace(" ", "-")
print(Task("Read Docs").slug)
# 输出: read-docs
# 示例类型:最小示例
最小示例 2:setter 校验
python
# 示例目标:setter 校验
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:完整可运行
class Task:
def __init__(self, priority: int) -> None:
self.priority = priority
@property
def priority(self) -> int:
return self._priority
@priority.setter
def priority(self, value: int) -> None:
if not 1 <= value <= 5:
raise ValueError("priority out of range")
self._priority = value
print(Task(3).priority)
# 输出: 3
# 示例类型:最小示例
实际场景示例
输入校验、派生字段、惰性计算和框架字段声明都可以使用属性协议;属性读取不应偷偷执行昂贵 I/O。
python
# 示例目标:缓存派生值
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行
from functools import cached_property
class Task:
def __init__(self, title: str) -> None:
self.title = title
@cached_property
def display(self) -> str:
return self.title.upper()
task = Task("read")
print(task.display)
# 输出: READ
# 示例类型:实际场景
关联/组合
组合 1:setter 使用内部字段
python
# 示例目标:setter 使用内部字段
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行
class Box:
def __init__(self) -> None:
self.value = 1
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, number: int) -> None:
self._value = number
box = Box()
box.value = 2
print(box.value)
# 输出: 2
# 示例类型:关联/组合
组合 2:只读属性
python
# 示例目标:只读属性
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行
class Task:
@property
def kind(self) -> str:
return "task"
print(Task().kind)
# 输出: task
# 示例类型:关联/组合
易错/易混
易错 1:setter 递归赋值
错误代码:
python
# 示例目标:setter 递归赋值(错误示范)
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行
class Task:
@property
def priority(self) -> int:
return self.priority
@priority.setter
def priority(self, value: int) -> None:
self.priority = value
# 示例类型:错误示范
实际结果:赋值时无限递归,最终 RecursionError。
修正代码:
python
# 示例目标:setter 递归赋值(修正)
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行
class Task:
@property
def priority(self) -> int:
return self._priority
@priority.setter
def priority(self, value: int) -> None:
self._priority = value
task = Task()
task.priority = 2
print(task.priority)
# 输出: 2
# 示例类型:易错修正
原因:setter 必须写入不同的内部字段。
易错 2:属性读取做网络请求
错误代码:
python
# 示例目标:属性读取做网络请求(错误示范)
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行
class User:
@property
def profile(self):
return fetch_profile()
# 示例类型:错误示范
实际结果:每次访问属性都可能触发网络,调用方无法预期。
修正代码:
python
# 示例目标:属性读取做网络请求(修正)
# 前置知识:4.1 类;4.4 魔术方法;3.7 异常。
# 代码状态:片段/按前置知识运行
class User:
def load_profile(self):
return fetch_profile()
# 示例类型:易错修正
原因:有 I/O 的操作使用显式方法名,让调用方知道成本。
练习
- 给 Task 增加只读属性
slug。 - 写一个 priority setter,拒绝 0 以下的值。
4.7 ABC、Protocol 与组合
前置知识:4.1~4.6;4.2 注解。
概念:ABC 是显式继承的抽象基类;Protocol 是结构化类型,只要对象有匹配方法就兼容。组合通过持有其他对象复用能力,通常比继承更灵活。
本单元新语法
| 语法形式 | 含义 |
|---|---|
class X(ABC): |
名义抽象基类 |
@abstractmethod |
强制子类实现 |
Protocol |
结构化接口 |
runtime_checkable |
允许 isinstance 检查成员 |
组合 |
通过属性持有依赖 |
最小示例
最小示例 1:Protocol 仓储
python
# 示例目标:Protocol 仓储
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:完整可运行
from typing import Protocol
class Repository(Protocol):
def save(self, title: str) -> str: ...
class MemoryRepository:
def save(self, title: str) -> str:
return "saved:" + title
def run(repo: Repository) -> str:
return repo.save("Read")
print(run(MemoryRepository()))
# 输出: saved:Read
# 示例类型:最小示例
最小示例 2:ABC 约束
python
# 示例目标:ABC 约束
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:完整可运行
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, message: str) -> None:
raise NotImplementedError
class ConsoleNotifier(Notifier):
def send(self, message: str) -> None:
print(message)
ConsoleNotifier().send("hello")
# 输出: hello
# 示例类型:最小示例
实际场景示例
领域层定义 Protocol,基础设施层实现它,服务通过构造器接收依赖;这样测试可注入内存实现。
python
# 示例目标:服务依赖协议
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行
from typing import Protocol
class Clock(Protocol):
def now(self) -> int: ...
class Service:
def __init__(self, clock: Clock) -> None:
self.clock = clock
def stamp(self) -> int:
return self.clock.now()
class FixedClock:
def now(self) -> int:
return 42
print(Service(FixedClock()).stamp())
# 输出: 42
# 示例类型:实际场景
关联/组合
组合 1:组合替代继承
python
# 示例目标:组合替代继承
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行
class Logger:
def log(self, message: str) -> None:
print(message)
class Service:
def __init__(self, logger: Logger) -> None:
self.logger = logger
def run(self) -> None:
self.logger.log("run")
Service(Logger()).run()
# 输出: run
# 示例类型:关联/组合
组合 2:runtime_checkable
python
# 示例目标:runtime_checkable
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行
from typing import Protocol, runtime_checkable
@runtime_checkable
class Named(Protocol):
name: str
class User:
name = "ann"
print(isinstance(User(), Named))
# 输出: True
# 示例类型:关联/组合
易错/易混
易错 1:把 Protocol 当运行时校验
错误代码:
python
# 示例目标:把 Protocol 当运行时校验(错误示范)
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行
from typing import Protocol
class Repo(Protocol):
def save(self, title: str) -> str: ...
class Bad:
def save(self, number: int) -> int:
return number
# 示例类型:错误示范
实际结果:静态检查器可能报错,但运行时不会自动拒绝 Bad。
修正代码:
python
# 示例目标:把 Protocol 当运行时校验(修正)
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行
class Bad:
def save(self, title: str) -> str:
return title
print(Bad().save("ok"))
# 输出: ok
# 示例类型:易错修正
原因:Protocol 主要服务静态检查,边界仍需运行时验证。
易错 2:继承具体类只为复用
错误代码:
python
# 示例目标:继承具体类只为复用(错误示范)
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行
class Logger:
def log(self, message: str) -> None:
print(message)
class Service(Logger):
pass
# 示例类型:错误示范
实际结果:Service 获得了 Logger 的接口,但两者没有真正的 is-a 关系。
修正代码:
python
# 示例目标:继承具体类只为复用(修正)
# 前置知识:4.1~4.6;4.2 注解。
# 代码状态:片段/按前置知识运行
class Logger:
def log(self, message: str) -> None:
print(message)
class Service:
def __init__(self, logger: Logger) -> None:
self.logger = logger
Service(Logger()).logger.log("ok")
# 输出: ok
# 示例类型:易错修正
原因:组合表达 has-a 关系,继承表达 is-a 关系。
练习
- 定义
RepositoryProtocol 和MemoryRepository实现。 - 写一个抽象 Notifier,并实现 ConsoleNotifier。
4.8 枚举、常量与状态建模
前置知识:4.5 dataclass;4.2 注解。
概念:Enum 用命名成员表达有限集合,避免魔法字符串。StrEnum 适合 JSON/API,IntEnum 与整数兼容,Flag 适合组合权限;非法成员值会在转换时尽早失败。
本单元新语法
| 语法形式 | 含义 |
|---|---|
class Status(Enum): |
枚举成员 |
StrEnum |
字符串兼容枚举 |
IntEnum |
整数兼容枚举 |
Flag |
可组合位标志 |
Member.value |
成员底层值 |
Status(raw) |
按值查找成员 |
最小示例
最小示例 1:任务状态
python
# 示例目标:任务状态
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:完整可运行
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
status = Status("done")
print(status.value)
# 输出: done
# 示例类型:最小示例
最小示例 2:枚举在匹配中
python
# 示例目标:枚举在匹配中
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:完整可运行
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
def label(status: Status) -> str:
match status:
case Status.DONE:
return "finished"
case _:
return "open"
print(label(Status.DONE))
# 输出: finished
# 示例类型:最小示例
实际场景示例
订单状态、任务状态、用户角色和事件类型使用枚举;数据库历史值变更仍需迁移和兼容处理。
python
# 示例目标:状态字段转换
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
def normalize(raw: str) -> Status:
return Status(raw)
print(normalize("pending"))
# 输出: pending
# 示例类型:实际场景
关联/组合
组合 1:IntFlag 权限
python
# 示例目标:IntFlag 权限
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行
from enum import IntFlag
class Permission(IntFlag):
READ = 1
WRITE = 2
print(Permission.READ | Permission.WRITE)
# 输出: Permission.READ|WRITE
# 示例类型:关联/组合
组合 2:成员名与值
python
# 示例目标:成员名与值
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行
from enum import StrEnum
class Status(StrEnum):
DONE = "done"
print(Status.DONE.name, Status.DONE.value)
# 输出: DONE done
# 示例类型:关联/组合
易错/易混
易错 1:用字符串比较代替枚举
错误代码:
python
# 示例目标:用字符串比较代替枚举(错误示范)
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行
status = "done"
if status == "Done":
print("finished")
# 示例类型:错误示范
实际结果:大小写不一致导致逻辑静默失败。
修正代码:
python
# 示例目标:用字符串比较代替枚举(修正)
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行
from enum import StrEnum
class Status(StrEnum):
DONE = "done"
status = Status("done")
print(status is Status.DONE)
# 输出: True
# 示例类型:易错修正
原因:枚举集中管理合法值和比较语义。
易错 2:把未知值强行当成员
错误代码:
python
# 示例目标:把未知值强行当成员(错误示范)
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行
from enum import StrEnum
class Status(StrEnum):
DONE = "done"
print(Status("unknown"))
# 示例类型:错误示范
实际结果:抛出 ValueError。
修正代码:
python
# 示例目标:把未知值强行当成员(修正)
# 前置知识:4.5 dataclass;4.2 注解。
# 代码状态:片段/按前置知识运行
from enum import StrEnum
class Status(StrEnum):
DONE = "done"
try:
Status("unknown")
except ValueError as exc:
print("invalid status", exc)
# 输出: invalid status 'unknown' is not a valid Status
# 示例类型:易错修正
原因:外部值转换应捕获并翻译错误。
练习
- 定义
Status枚举,包含 pending 和 done。 - 用 match 对 Status.DONE 返回 finished。
4.9 泛型、TypedDict、Literal 与静态检查
前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
概念:泛型保持容器和函数中的类型关系;TypedDict 描述字典键;Literal 限定字面量;Annotated 附加元数据。它们主要服务静态检查,运行时通常不验证。
本单元新语法
| 语法形式 | 含义 |
|---|---|
TypeVar("T") |
类型变量 |
Generic[T] |
泛型类 |
TypedDict |
带键类型的字典 |
Literal[...] |
字面量集合 |
Annotated[T, metadata] |
给类型附加元数据 |
TYPE_CHECKING |
仅静态检查时导入 |
最小示例
最小示例 1:泛型 first
python
# 示例目标:泛型 first
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:完整可运行
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
print(first([1, 2]))
print(first(["a"]))
# 输出: 1
# 输出: a
# 示例类型:最小示例
最小示例 2:TypedDict
python
# 示例目标:TypedDict
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:完整可运行
from typing import TypedDict
class TaskPayload(TypedDict):
title: str
done: bool
payload: TaskPayload = {"title": "Read", "done": False}
print(payload["title"])
# 输出: Read
# 示例类型:最小示例
实际场景示例
公共 SDK、通用仓储和 API 数据结构需要精确类型;运行时请求校验仍交给 Pydantic。
python
# 示例目标:Literal 工作流
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行
from typing import Literal
def priority(value: Literal["low", "high"]) -> str:
return value
print(priority("high"))
# 输出: high
# 示例类型:实际场景
关联/组合
组合 1:TYPE_CHECKING 导入
python
# 示例目标:TYPE_CHECKING 导入
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from decimal import Decimal
def amount(value: "Decimal") -> str:
return str(value)
# 示例类型:关联/组合
组合 2:NotRequired
python
# 示例目标:NotRequired
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行
from typing import NotRequired, TypedDict
class Payload(TypedDict):
title: str
notes: NotRequired[str]
payload: Payload = {"title": "Read"}
print(payload)
# 输出: {'title': 'Read'}
# 示例类型:关联/组合
易错/易混
易错 1:把 TypedDict 当运行时校验
错误代码:
python
# 示例目标:把 TypedDict 当运行时校验(错误示范)
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行
from typing import TypedDict
class Payload(TypedDict):
title: str
data: Payload = {"title": 123}
print(data)
# 示例类型:错误示范
实际结果:运行时不会报错,类型检查器才会提示。
修正代码:
python
# 示例目标:把 TypedDict 当运行时校验(修正)
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行
from typing import TypedDict
class Payload(TypedDict):
title: str
data = {"title": 123}
if not isinstance(data["title"], str):
raise TypeError("title must be str")
print(data)
# 示例类型:易错修正
原因:静态类型不等于运行时校验。
易错 2:用 Any 逃避检查
错误代码:
python
# 示例目标:用 Any 逃避检查(错误示范)
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行
from typing import Any
def parse(value: Any) -> Any:
return value
# 示例类型:错误示范
实际结果:任何调用都通过,错误推迟到运行时。
修正代码:
python
# 示例目标:用 Any 逃避检查(修正)
# 前置知识:4.2 注解;4.7 Protocol;4.8 枚举。
# 代码状态:片段/按前置知识运行
def parse(value: str) -> str:
return value.strip()
print(parse(" hi "))
# 输出: hi
# 示例类型:易错修正
原因:尽量缩小 Any 范围,边界使用明确类型。
练习
- 写一个泛型
first(items: list[T]) -> T。 - 用 TypedDict 描述
{title: str, done: bool}。
4.10 复制、相等、哈希与 slots
前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
概念 :相等性比较值,身份比较对象,哈希用于快速查找。值对象若可哈希,应保证相等对象哈希相同;__slots__ 限制实例属性并减少内存,但牺牲动态属性和部分缓存能力。
本单元新语法
| 语法形式 | 含义 |
|---|---|
copy.copy |
浅复制 |
copy.deepcopy |
递归复制 |
__eq__ |
相等性 |
__hash__ |
哈希 |
__slots__ |
固定实例属性 |
weakref |
不阻止回收的弱引用 |
最小示例
最小示例 1:deepcopy 隔离嵌套
python
# 示例目标:deepcopy 隔离嵌套
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:完整可运行
from copy import deepcopy
original = {"tags": ["a"]}
copy = deepcopy(original)
copy["tags"].append("b")
print(original, copy)
# 输出: {'tags': ['a']} {'tags': ['a', 'b']}
# 示例类型:最小示例
最小示例 2:slots 减少属性字典
python
# 示例目标:slots 减少属性字典
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:完整可运行
class Point:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
print(Point(1, 2).x)
# 输出: 1
# 示例类型:最小示例
实际场景示例
缓存键、集合成员和领域值对象需要正确哈希;大量小对象可用 slots 降低内存。
python
# 示例目标:可哈希值对象
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行
class Point:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def __eq__(self, other: object) -> bool:
return isinstance(other, Point) and (self.x, self.y) == (other.x, other.y)
def __hash__(self) -> int:
return hash((self.x, self.y))
print(len({Point(1, 2), Point(1, 2)}))
# 输出: 1
# 示例类型:实际场景
关联/组合
组合 1:浅复制嵌套
python
# 示例目标:浅复制嵌套
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行
from copy import copy
original = {"items": [1]}
clone = copy(original)
clone["items"].append(2)
print(original)
# 输出: {'items': [1, 2]}
# 示例类型:关联/组合
组合 2:弱引用
python
# 示例目标:弱引用
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行
import weakref
class Task:
pass
task = Task()
ref = weakref.ref(task)
print(ref() is task)
# 输出: True
# 示例类型:关联/组合
易错/易混
易错 1:自定义 eq 不自定义 hash
错误代码:
python
# 示例目标:自定义 eq 不自定义 hash(错误示范)
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行
class Task:
def __eq__(self, other: object) -> bool:
return True
print(hash(Task()))
# 示例类型:错误示范
实际结果:抛出 TypeError:unhashable type。
修正代码:
python
# 示例目标:自定义 eq 不自定义 hash(修正)
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行
class Task:
def __eq__(self, other: object) -> bool:
return isinstance(other, Task)
def __hash__(self) -> int:
return 1
print(hash(Task()))
# 输出: 1
# 示例类型:易错修正
原因:相等性和哈希必须保持一致。
易错 2:对 slots 对象添加未声明属性
错误代码:
python
# 示例目标:对 slots 对象添加未声明属性(错误示范)
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行
class Task:
__slots__ = ("title",)
def __init__(self) -> None:
self.title = "Read"
task = Task()
task.owner = "ann"
# 示例类型:错误示范
实际结果:抛出 AttributeError:没有 owner 槽位。
修正代码:
python
# 示例目标:对 slots 对象添加未声明属性(修正)
# 前置知识:4.4 魔术方法;4.5 dataclass;第 1 卷可变性。
# 代码状态:片段/按前置知识运行
class Task:
__slots__ = ("title", "owner")
def __init__(self) -> None:
self.title = "Read"
self.owner = "ann"
task = Task()
print(task.owner)
# 输出: ann
# 示例类型:易错修正
原因:slots 必须提前列出所有实例属性。
练习
- 用 deepcopy 隔离嵌套列表。
- 用 slots 定义只有 x、y 的 Point。
4.11 OOP 与类型项目线:TaskFlow v04
前置知识:4.1~4.10。
概念:项目线把任务从字典升级为 frozen dataclass,把状态升级为 StrEnum,把仓储需求表达为 Protocol;服务只依赖协议,不依赖具体存储。
本单元新语法
| 语法形式 | 含义 |
|---|---|
@dataclass(frozen=True) |
不可变任务 |
StrEnum |
有限状态 |
Protocol |
仓储接口 |
replace |
派生新状态 |
类型注解 |
接口契约 |
最小示例
最小示例 1:任务模型
python
# 示例目标:任务模型
# 前置知识:4.1~4.10。
# 代码状态:完整可运行
from dataclasses import dataclass
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
@dataclass(frozen=True)
class Task:
id: str
title: str
status: Status = Status.PENDING
def complete(self) -> "Task":
from dataclasses import replace
return replace(self, status=Status.DONE)
print(Task("a", "Read").complete())
# 输出包含: status=<Status.DONE: 'done'>
# 示例类型:最小示例
最小示例 2:仓储协议
python
# 示例目标:仓储协议
# 前置知识:4.1~4.10。
# 代码状态:完整可运行
from typing import Protocol
class Repository(Protocol):
def add(self, title: str) -> str: ...
class MemoryRepository:
def add(self, title: str) -> str:
return "stored:" + title
print(MemoryRepository().add("Read"))
# 输出: stored:Read
# 示例类型:最小示例
实际场景示例
这是从"脚本 + 字典"到"领域模型 + 接口"的关键跃迁;后续标准库和 Web 层都依赖这组核心类型。
python
# 示例目标:服务依赖协议
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行
from typing import Protocol
class Repository(Protocol):
def add(self, title: str) -> str: ...
class Service:
def __init__(self, repo: Repository) -> None:
self.repo = repo
def create(self, title: str) -> str:
return self.repo.add(title.strip())
class MemoryRepository:
def add(self, title: str) -> str:
return title
print(Service(MemoryRepository()).create(" Read "))
# 输出: Read
# 示例类型:实际场景
关联/组合
组合 1:用枚举状态
python
# 示例目标:用枚举状态
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
print(Status.PENDING)
# 输出: pending
# 示例类型:关联/组合
组合 2:用 replace 完成任务
python
# 示例目标:用 replace 完成任务
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass, replace
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
@dataclass(frozen=True)
class Task:
title: str
status: Status = Status.PENDING
print(replace(Task("Read"), status=Status.DONE).status)
# 输出: done
# 示例类型:关联/组合
易错/易混
易错 1:服务直接依赖具体仓储
错误代码:
python
# 示例目标:服务直接依赖具体仓储(错误示范)
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行
class Service:
def __init__(self) -> None:
self.repo = SqliteRepository()
# 示例类型:错误示范
实际结果:测试时无法替换存储,服务与数据库耦合。
修正代码:
python
# 示例目标:服务直接依赖具体仓储(修正)
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行
class Service:
def __init__(self, repo) -> None:
self.repo = repo
# 示例类型:易错修正
原因:依赖从构造器注入,生产/测试可替换。
易错 2:状态仍用裸字符串
错误代码:
python
# 示例目标:状态仍用裸字符串(错误示范)
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行
task = {"status": "done"}
if task["status"] == "Done":
print("ok")
# 示例类型:错误示范
实际结果:大小写不一致导致条件失败。
修正代码:
python
# 示例目标:状态仍用裸字符串(修正)
# 前置知识:4.1~4.10。
# 代码状态:片段/按前置知识运行
from enum import StrEnum
class Status(StrEnum):
DONE = "done"
task = {"status": Status.DONE}
print(task["status"] is Status.DONE)
# 输出: True
# 示例类型:易错修正
原因:枚举把合法值和比较语义集中管理。
练习
- 用 frozen dataclass 定义 Task,并在 complete 中返回新对象。
- 定义 Repository Protocol 和 MemoryRepository。
4.12 dataclass 进阶:校验、字段策略与继承
前置知识:4.5 dataclass;4.2 注解;3.7 异常。
概念 :dataclass 的字段可以分别控制默认值、比较、repr 和初始化方式。__post_init__ 适合派生字段和轻量不变量校验;kw_only 让构造参数必须使用关键字,减少位置错误。
本单元新语法
| 语法形式 | 含义 |
|---|---|
field(default_factory=...) |
每实例默认值 |
field(compare=False) |
排除相等性比较 |
__post_init__ |
初始化后的派生/校验 |
kw_only=True |
仅关键字构造 |
inheritance |
子类字段继承 |
asdict(obj) |
递归转字典 |
最小示例
最小示例 1:post_init 派生字段
python
# 示例目标:post_init 派生字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:完整可运行
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
tags: list[str] = field(default_factory=list)
tag_count: int = field(init=False)
def __post_init__(self) -> None:
self.tag_count = len(self.tags)
print(Task("Read", ["work"]).tag_count)
# 输出: 1
# 示例类型:最小示例
最小示例 2:仅关键字字段
python
# 示例目标:仅关键字字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:完整可运行
from dataclasses import dataclass
@dataclass(kw_only=True)
class Task:
title: str
urgent: bool = False
print(Task(title="Read", urgent=True))
# 输出: Task(title='Read', urgent=True)
# 示例类型:最小示例
最小示例 3:asdict
python
# 示例目标:asdict
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:完整可运行
from dataclasses import asdict, dataclass
@dataclass
class Task:
id: str
title: str
print(asdict(Task("a", "Read")))
# 输出: {'id': 'a', 'title': 'Read'}
# 示例类型:最小示例
实际场景示例
API DTO、配置快照和测试数据常用 dataclass;外部 JSON 仍要先验证类型,再转换成不可信字段受限的领域对象。
python
# 示例目标:比较忽略内部字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
revision: int = field(default=0, compare=False)
print(Task("Read", 1) == Task("Read", 2))
# 输出: True
# 示例类型:实际场景
关联/组合
组合 1:继承字段
python
# 示例目标:继承字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass
@dataclass
class Base:
title: str
@dataclass
class Urgent(Base):
priority: int = 1
print(Urgent("Read", 2))
# 输出: Urgent(title='Read', priority=2)
# 示例类型:关联/组合
组合 2:默认值依赖
python
# 示例目标:默认值依赖
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass, field
@dataclass
class Box:
items: tuple[str, ...] = field(default_factory=tuple)
print(Box().items)
# 输出: ()
# 示例类型:关联/组合
组合 3:只读派生字段
python
# 示例目标:只读派生字段
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
slug: str = field(init=False)
def __post_init__(self) -> None:
self.slug = self.title.lower().replace(" ", "-")
print(Task("Read Docs").slug)
# 输出: read-docs
# 示例类型:关联/组合
易错/易混
易错 1:在 post_init 中修改 frozen 字段
错误代码:
python
# 示例目标:在 post_init 中修改 frozen 字段(错误示范)
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass
@dataclass(frozen=True)
class Task:
title: str
slug: str = ""
def __post_init__(self) -> None:
self.slug = self.title.lower()
# 示例类型:错误示范
实际结果:抛出 FrozenInstanceError:frozen 对象不能直接赋值。
修正代码:
python
# 示例目标:在 post_init 中修改 frozen 字段(修正)
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Task:
title: str
slug: str = field(init=False)
def __post_init__(self) -> None:
object.__setattr__(self, "slug", self.title.lower())
print(Task("Read").slug)
# 输出: read
# 示例类型:易错修正
原因 :frozen 派生字段使用 object.setattr,并把它限制在初始化阶段。
易错 2:依赖子类字段顺序
错误代码:
python
# 示例目标:依赖子类字段顺序(错误示范)
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass
@dataclass
class Base:
title: str
@dataclass
class Child(Base):
title: str = "default"
# 示例类型:错误示范
实际结果:可能触发 TypeError:默认值字段覆盖无默认值父类字段。
修正代码:
python
# 示例目标:依赖子类字段顺序(修正)
# 前置知识:4.5 dataclass;4.2 注解;3.7 异常。
# 代码状态:片段/按前置知识运行
from dataclasses import dataclass
@dataclass
class Base:
title: str
@dataclass
class Child(Base):
title: str
priority: int = 1
print(Child("Read"))
# 输出: Child(title='Read', priority=1)
# 示例类型:易错修正
原因:继承时保持字段顺序和默认值规则一致。
练习
- 用 dataclass 和 post_init 计算 tag_count。
- 定义一个 kw_only dataclass,包含 title 和 urgent。
4.13 对象协议进阶:可调用、索引与上下文
前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
概念 :除 __eq__ 和 __repr__ 外,对象还可以实现调用、索引、成员测试、加法和上下文管理协议。这些协议让自定义对象自然融入 Python 语法,但每个协议都要保持行为一致和可预测。
本单元新语法
| 语法形式 | 含义 |
|---|---|
__call__(self, ...) |
对象可像函数一样调用 |
__getitem__(self, key) |
支持索引/切片 |
__contains__(self, item) |
支持 in |
__add__(self, other) |
支持 + |
__enter__/__exit__ |
支持 with |
total_ordering |
生成比较方法 |
最小示例
最小示例 1:可调用对象
python
# 示例目标:可调用对象
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:完整可运行
class Multiplier:
def __init__(self, factor: int) -> None:
self.factor = factor
def __call__(self, value: int) -> int:
return value * self.factor
triple = Multiplier(3)
print(triple(4))
# 输出: 12
# 示例类型:最小示例
最小示例 2:索引协议
python
# 示例目标:索引协议
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:完整可运行
class Numbers:
def __init__(self, values: list[int]) -> None:
self.values = values
def __getitem__(self, index: int) -> int:
return self.values[index]
print(Numbers([10, 20])[1])
# 输出: 20
# 示例类型:最小示例
最小示例 3:上下文管理协议
python
# 示例目标:上下文管理协议
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:完整可运行
class Resource:
def __enter__(self):
print("open")
return "value"
def __exit__(self, exc_type, exc, tb):
print("close")
with Resource() as value:
print(value)
# 输出: open
# 输出: value
# 输出: close
# 示例类型:最小示例
实际场景示例
领域对象、查询结果包装器、连接池和上下文工具都可能实现这些协议;接口越像内置对象,越要保证错误和边界行为一致。
python
# 示例目标:任务集合可迭代
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行
class Tasks:
def __init__(self, items: list[str]) -> None:
self.items = items
def __iter__(self):
return iter(self.items)
def __len__(self) -> int:
return len(self.items)
tasks = Tasks(["a", "b"])
print(len(tasks), list(tasks))
# 输出: 2 ['a', 'b']
# 示例类型:实际场景
关联/组合
组合 1:加法协议
python
# 示例目标:加法协议
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行
class Money:
def __init__(self, cents: int) -> None:
self.cents = cents
def __add__(self, other: "Money") -> "Money":
return Money(self.cents + other.cents)
print(Money(100) + Money(50))
# 输出: <__main__.Money object at ...>
# 示例类型:关联/组合
组合 2:成员测试
python
# 示例目标:成员测试
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行
class Tags:
def __init__(self, values: list[str]) -> None:
self.values = values
def __contains__(self, item: str) -> bool:
return item in self.values
print("work" in Tags(["work"]))
# 输出: True
# 示例类型:关联/组合
组合 3:排序协议
python
# 示例目标:排序协议
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, number: int) -> None:
self.number = number
def __eq__(self, other: object) -> bool:
return isinstance(other, Version) and self.number == other.number
def __lt__(self, other: "Version") -> bool:
return self.number < other.number
print(Version(2) > Version(1))
# 输出: True
# 示例类型:关联/组合
易错/易混
易错 1:只实现 getitem 却假设长度
错误代码:
python
# 示例目标:只实现 getitem 却假设长度(错误示范)
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行
class Values:
def __getitem__(self, index):
return index
print(len(Values()))
# 示例类型:错误示范
实际结果 :抛出 TypeError:对象没有 len。
修正代码:
python
# 示例目标:只实现 getitem 却假设长度(修正)
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行
class Values:
def __getitem__(self, index):
return index
def __len__(self):
return 3
print(len(Values()))
# 输出: 3
# 示例类型:易错修正
原因:每个语法协议对应明确的魔术方法。
易错 2:exit 返回 True 吞掉异常
错误代码:
python
# 示例目标:__exit__ 返回 True 吞掉异常(错误示范)
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行
class Hidden:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return True
with Hidden():
raise ValueError("hidden")
# 示例类型:错误示范
实际结果:主体异常被静默吞掉,调用方无法感知失败。
修正代码:
python
# 示例目标:__exit__ 返回 True 吞掉异常(修正)
# 前置知识:4.4 魔术方法;3.8 with;4.12 dataclass。
# 代码状态:片段/按前置知识运行
class Visible:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
try:
with Visible():
raise ValueError("visible")
except ValueError as exc:
print(exc)
# 输出: visible
# 示例类型:易错修正
原因 :只有明确需要抑制时才让 exit 返回 True。
练习
- 实现 call,让对象把数字翻倍。
- 实现 contains,让对象支持成员测试。
4.14 typing 进阶:Callable、TypeVar 与 overload
前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
概念 :类型系统不仅要描述值,还要描述函数、类型之间的关系和重载。Callable 表示函数签名,TypeVar 保留输入输出关系,overload 只提供静态签名,最后一个实现仍然是运行时真正执行的函数。
本单元新语法
| 语法形式 | 含义 |
|---|---|
Callable[[A], B] |
函数类型 |
TypeVar(bound=Base) |
受限类型变量 |
NewType("Id", str) |
语义化基础类型 |
@overload |
静态重载声明 |
ParamSpec |
转发参数签名 |
Literal[...] |
限定字面量 |
最小示例
最小示例 1:Callable 参数
python
# 示例目标:Callable 参数
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:完整可运行
from typing import Callable
def apply(fn: Callable[[int], int], value: int) -> int:
return fn(value)
print(apply(abs, -3))
# 输出: 3
# 示例类型:最小示例
最小示例 2:NewType
python
# 示例目标:NewType
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:完整可运行
from typing import NewType
TaskId = NewType("TaskId", str)
task_id = TaskId("t1")
print(task_id)
# 输出: t1
# 示例类型:最小示例
最小示例 3:TypeVar bound
python
# 示例目标:TypeVar bound
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:完整可运行
from typing import TypeVar
class Base:
pass
T = TypeVar("T", bound=Base)
def identity(value: T) -> T:
return value
print(identity(Base()) is not None)
# 输出: True
# 示例类型:最小示例
实际场景示例
公共 SDK、函数式管道和通用仓储需要精确函数签名;业务内部不要为了类型而堆叠泛型。
python
# 示例目标:重载的静态描述
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行
from typing import overload
@overload
def parse(value: str) -> int: ...
@overload
def parse(value: bytes) -> int: ...
def parse(value):
return int(value)
print(parse("3"))
# 输出: 3
# 示例类型:实际场景
关联/组合
组合 1:Callable 列表
python
# 示例目标:Callable 列表
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行
from typing import Callable
handlers: list[Callable[[], str]] = [lambda: "a", lambda: "b"]
print([handler() for handler in handlers])
# 输出: ['a', 'b']
# 示例类型:关联/组合
组合 2:TypeVar 保持关系
python
# 示例目标:TypeVar 保持关系
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
print(first(["a"]))
# 输出: a
# 示例类型:关联/组合
组合 3:字面量类型
python
# 示例目标:字面量类型
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行
from typing import Literal
def priority(value: Literal["low", "high"]) -> str:
return value
print(priority("high"))
# 输出: high
# 示例类型:关联/组合
易错/易混
易错 1:把 cast 当转换
错误代码:
python
# 示例目标:把 cast 当转换(错误示范)
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行
from typing import cast
def parse(value: str) -> int:
return cast(int, value)
# 示例类型:错误示范
实际结果:cast 运行时仍返回原字符串,后续运算会失败。
修正代码:
python
# 示例目标:把 cast 当转换(修正)
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行
def parse(value: str) -> int:
return int(value)
print(parse("3"))
# 输出: 3
# 示例类型:易错修正
原因:cast 只是静态承诺,不执行转换。
易错 2:为 overload 写多个实现
错误代码:
python
# 示例目标:为 overload 写多个实现(错误示范)
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行
from typing import overload
@overload
def parse(value: str) -> int: ...
def parse(value):
return int(value)
# 示例类型:错误示范
实际结果:可以运行,但 overload 函数体应是省略号,只有一个真实实现。
修正代码:
python
# 示例目标:为 overload 写多个实现(修正)
# 前置知识:4.2 注解;4.7 Protocol;4.9 泛型。
# 代码状态:片段/按前置知识运行
from typing import overload
@overload
def parse(value: str) -> int: ...
@overload
def parse(value: bytes) -> int: ...
def parse(value):
return int(value)
print(parse("3"))
# 输出: 3
# 示例类型:易错修正
原因:重载是静态接口,不是运行时多分派。
练习
- 定义一个接收
Callable[[int], int]的高阶函数。 - 用 TypeVar 编写返回列表首元素的泛型函数。
4.15 泛型接口与依赖注入组合
前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
概念:协议可以带类型变量,表达"仓储保存某种实体";服务依赖协议而不依赖具体实现。泛型接口适合通用组件,但业务用例仍应保持简单、显式。
本单元新语法
| 语法形式 | 含义 |
|---|---|
Generic[T] |
泛型接口 |
Protocol + TypeVar |
带类型的结构化接口 |
constructor injection |
构造器注入依赖 |
Repository[T] |
泛型仓储 |
fake implementation |
测试替身 |
type narrowing |
按分支缩小类型 |
最小示例
最小示例 1:泛型协议
python
# 示例目标:泛型协议
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:完整可运行
from typing import Protocol, TypeVar
T = TypeVar("T")
class Store(Protocol[T]):
def save(self, value: T) -> None: ...
class MemoryStore:
def __init__(self) -> None:
self.values = []
def save(self, value: int) -> None:
self.values.append(value)
store = MemoryStore()
store.save(3)
print(store.values)
# 输出: [3]
# 示例类型:最小示例
最小示例 2:服务依赖协议
python
# 示例目标:服务依赖协议
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:完整可运行
from typing import Protocol
class Repo(Protocol):
def add(self, title: str) -> str: ...
class Service:
def __init__(self, repo: Repo) -> None:
self.repo = repo
def create(self, title: str) -> str:
return self.repo.add(title.strip())
class FakeRepo:
def add(self, title: str) -> str:
return "fake:" + title
print(Service(FakeRepo()).create(" Read "))
# 输出: fake:Read
# 示例类型:最小示例
最小示例 3:测试替身
python
# 示例目标:测试替身
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:完整可运行
class FakeClock:
def now(self) -> int:
return 42
class Service:
def __init__(self, clock) -> None:
self.clock = clock
def stamp(self) -> int:
return self.clock.now()
print(Service(FakeClock()).stamp())
# 输出: 42
# 示例类型:最小示例
实际场景示例
分层应用通过协议隔离数据库/HTTP/时钟;测试注入内存实现,生产注入真实适配器。
python
# 示例目标:泛型仓储的职责
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行
from typing import Protocol, TypeVar
T = TypeVar("T")
class Reader(Protocol[T]):
def get(self, key: str) -> T | None: ...
class DictReader:
def __init__(self) -> None:
self.data = {"a": 1}
def get(self, key: str) -> int | None:
return self.data.get(key)
reader: Reader[int] = DictReader()
print(reader.get("a"))
# 输出: 1
# 示例类型:实际场景
关联/组合
组合 1:依赖注入函数
python
# 示例目标:依赖注入函数
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行
def build_service(repo):
return {"repo": repo}
service = build_service({"add": str})
print(service["repo"]["add"]("x"))
# 输出: x
# 示例类型:关联/组合
组合 2:泛型容器
python
# 示例目标:泛型容器
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
print(first([1]))
# 输出: 1
# 示例类型:关联/组合
组合 3:协议作为边界
python
# 示例目标:协议作为边界
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行
from typing import Protocol
class Notifier(Protocol):
def send(self, text: str) -> None: ...
class Console:
def send(self, text: str) -> None:
print(text)
Console().send("hello")
# 输出: hello
# 示例类型:关联/组合
易错/易混
易错 1:把具体数据库放进领域对象
错误代码:
python
# 示例目标:把具体数据库放进领域对象(错误示范)
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行
class Task:
def save(self):
return database.connect("tasks.db")
# 示例类型:错误示范
实际结果:领域对象与数据库驱动绑定,测试和替换困难。
修正代码:
python
# 示例目标:把具体数据库放进领域对象(修正)
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行
class Task:
pass
def save(repository, task):
return repository.save(task)
# 示例类型:易错修正
原因:领域对象只表达业务状态,持久化通过仓储边界完成。
易错 2:为每个类都创建协议
错误代码:
python
# 示例目标:为每个类都创建协议(错误示范)
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行
class Everything(Protocol):
def do(self): ...
# 示例类型:错误示范
实际结果:接口膨胀,阅读和实现成本上升。
修正代码:
python
# 示例目标:为每个类都创建协议(修正)
# 前置知识:4.7 Protocol;4.11 项目模型;4.14 typing。
# 代码状态:片段/按前置知识运行
class Repo(Protocol):
def save(self, title: str) -> str: ...
# 示例类型:易错修正
原因:只在真实变化点和测试边界抽象。
练习
- 定义
Store[T]协议,并提供内存实现。 - 写一个依赖 Clock 协议的服务。
本卷综合实验室:类型化任务仓储
实验目标
用 dataclass、Enum、Protocol、类型注解和不可变值对象,把任务数据模型与存储实现分开。这个实验强调接口和边界,不追求真实数据库。
完整实现
python
# 示例目标:类型化任务仓储
# 前置知识:第 4 卷 dataclass、Enum、Protocol、typing、property
# 代码状态:完整可运行
from dataclasses import dataclass, field, replace
from enum import StrEnum
from typing import Protocol
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
@dataclass(frozen=True)
class Task:
id: str
title: str
status: Status = Status.PENDING
tags: tuple[str, ...] = ()
def __post_init__(self):
if not self.title.strip():
raise ValueError("title required")
def complete(self):
return replace(self, status=Status.DONE)
class Repository(Protocol):
def add(self, task: Task) -> Task: ...
def update(self, task: Task) -> Task: ...
def get(self, task_id: str) -> Task | None: ...
def list(self) -> list[Task]: ...
class MemoryRepository:
def __init__(self):
self.items = {}
def add(self, task: Task) -> Task:
if task.id in self.items:
raise ValueError("duplicate id")
self.items[task.id] = task
return task
def get(self, task_id: str) -> Task | None:
return self.items.get(task_id)
def update(self, task: Task) -> Task:
self.items[task.id] = task
return task
def list(self) -> list[Task]:
return sorted(self.items.values(), key=lambda task: task.id)
class TaskService:
def __init__(self, repository: Repository):
self.repository = repository
def create(self, task_id: str, title: str, tags=()) -> Task:
task = Task(task_id, title.strip(), tags=tuple(tags))
return self.repository.add(task)
def complete(self, task_id: str) -> Task:
task = self.repository.get(task_id)
if task is None:
raise LookupError("task not found")
completed = task.complete()
self.repository.update(completed)
return completed
repository = MemoryRepository()
service = TaskService(repository)
task = service.create("t1", " Read ", tags=("work",))
print(task)
print(service.complete("t1"))
# 输出:
# Task(id='t1', title='Read', status=<Status.PENDING: 'pending'>, tags=('work',))
# Task(id='t1', title='Read', status=<Status.DONE: 'done'>, tags=('work',))
关键解释
@dataclass(frozen=True)让任务成为值对象;complete()不修改旧对象,而是replace出新对象。__post_init__只做轻量不变量校验,外部 JSON 仍需专门解析/验证。Repository是结构化接口,内存实现和未来 SQLite 实现都可以满足。TaskService只依赖 Protocol,不依赖具体存储;这是依赖倒置的最小形态。list()排序使用命名函数,保持输出稳定,便于测试和 diff。
本卷关联
- 类与初始化来自 4.1,注解来自 4.2。
- 继承/组合来自 4.3,魔术方法来自 4.4。
- dataclass 进阶来自 4.12,Protocol/泛型来自 4.7/4.15。
验收标准
- 空标题在创建时失败。
- 完成任务不修改原对象。
- 重复 ID 被拒绝。
- 服务只依赖仓储接口,可替换内存实现。
复习问答
问:为什么 Task 不用可变 dataclass?
答:任务状态变化可以用 replace 表达新快照,避免共享对象在缓存、日志和测试间被悄悄修改。
问:为什么 TypeVar 没有出现在这个实验里?
答:泛型是通用组件工具,当前任务仓储只需要具体 Task;不要为了类型而增加无实际收益的泛型参数。
本卷练习答案
先独立完成练习,再展开答案;答案只使用本卷及之前介绍过的知识。
4.1 类、实例与方法 练习答案
练习 1 :写一个 Task 类,包含 title 和 done,并实现 complete 方法。
python
class Task:
def __init__(self, title):
self.title = title
self.done = False
def complete(self):
self.done = True
t = Task("Read")
t.complete()
print(t.done)
练习 2 :写一个类方法 empty() 创建标题为 untitled 的实例。
python
class Task:
def __init__(self, title):
self.title = title
@classmethod
def empty(cls):
return cls("untitled")
print(Task.empty().title)
4.2 类型注解与函数接口 练习答案
练习 1 :给 double(value) 加参数和返回值注解。
python
def double(value: int) -> int:
return value * 2
print(double(3))
练习 2 :写一个返回 str | None 的查找函数。
python
def find(items: list[str], target: str) -> str | None:
return target if target in items else None
print(find([], "a"))
4.3 继承、MRO 与 super 练习答案
练习 1:写一个父类和子类,子类在 describe 中调用 super。
python
class Base:
def describe(self) -> str:
return "base"
class Child(Base):
def describe(self) -> str:
return "child " + super().describe()
print(Child().describe())
练习 2:打印一个类的 MRO 名称。
python
class A:
pass
class B(A):
pass
print([c.__name__ for c in B.__mro__])
4.4 魔术方法与对象协议 练习答案
练习 1 :让 Task 的 == 按 title 比较。
python
class Task:
def __init__(self, title: str) -> None:
self.title = title
def __eq__(self, other: object) -> bool:
return isinstance(other, Task) and self.title == other.title
print(Task("a") == Task("a"))
练习 2 :实现 __len__,让 len(Tags(["a"])) 返回 1。
python
class Tags:
def __init__(self, items: list[str]) -> None:
self.items = items
def __len__(self) -> int:
return len(self.items)
print(len(Tags(["a"])))
4.5 dataclass、不可变对象与默认工厂 练习答案
练习 1:定义包含 id、title、tags 的 dataclass,tags 使用默认工厂。
python
from dataclasses import dataclass, field
@dataclass
class Task:
id: str
title: str
tags: list[str] = field(default_factory=list)
print(Task("a", "Read"))
练习 2 :用 replace 从 frozen Task 生成 done=True 的新对象。
python
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Task:
title: str
done: bool = False
print(replace(Task("Read"), done=True))
4.6 property、描述符与属性校验 练习答案
练习 1 :给 Task 增加只读属性 slug。
python
class Task:
def __init__(self, title: str) -> None:
self.title = title
@property
def slug(self) -> str:
return self.title.lower().replace(" ", "-")
print(Task("Read Docs").slug)
练习 2:写一个 priority setter,拒绝 0 以下的值。
python
class Task:
@property
def priority(self) -> int:
return self._priority
@priority.setter
def priority(self, value: int) -> None:
if value < 0:
raise ValueError("negative")
self._priority = value
t = Task()
t.priority = 2
print(t.priority)
4.7 ABC、Protocol 与组合 练习答案
练习 1 :定义 Repository Protocol 和 MemoryRepository 实现。
python
from typing import Protocol
class Repository(Protocol):
def save(self, title: str) -> str: ...
class MemoryRepository:
def save(self, title: str) -> str:
return title
print(MemoryRepository().save("x"))
练习 2:写一个抽象 Notifier,并实现 ConsoleNotifier。
python
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, message: str) -> None: ...
class ConsoleNotifier(Notifier):
def send(self, message: str) -> None:
print(message)
ConsoleNotifier().send("hi")
4.8 枚举、常量与状态建模 练习答案
练习 1 :定义 Status 枚举,包含 pending 和 done。
python
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
print(Status("pending"))
练习 2:用 match 对 Status.DONE 返回 finished。
python
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
status = Status.DONE
print("finished" if status is Status.DONE else "open")
4.9 泛型、TypedDict、Literal 与静态检查 练习答案
练习 1 :写一个泛型 first(items: list[T]) -> T。
python
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
print(first(["a"]))
练习 2 :用 TypedDict 描述 {title: str, done: bool}。
python
from typing import TypedDict
class Payload(TypedDict):
title: str
done: bool
p: Payload = {"title": "Read", "done": False}
print(p)
4.10 复制、相等、哈希与 slots 练习答案
练习 1:用 deepcopy 隔离嵌套列表。
python
from copy import deepcopy
original = [[1]]
clone = deepcopy(original)
clone[0].append(2)
print(original)
练习 2:用 slots 定义只有 x、y 的 Point。
python
class Point:
__slots__ = ("x", "y")
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
print(Point(1, 2).x)
4.11 OOP 与类型项目线:TaskFlow v04 练习答案
练习 1:用 frozen dataclass 定义 Task,并在 complete 中返回新对象。
python
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Task:
title: str
done: bool = False
def complete(self):
return replace(self, done=True)
print(Task("Read").complete())
练习 2:定义 Repository Protocol 和 MemoryRepository。
python
from typing import Protocol
class Repository(Protocol):
def add(self, title: str) -> str: ...
class MemoryRepository:
def add(self, title: str) -> str:
return title
print(MemoryRepository().add("Read"))
4.12 dataclass 进阶:校验、字段策略与继承 练习答案
练习 1 :用 dataclass 和 post_init 计算 tag_count。
python
from dataclasses import dataclass, field
@dataclass
class Task:
tags: list[str] = field(default_factory=list)
count: int = field(init=False)
def __post_init__(self):
self.count = len(self.tags)
print(Task(["a", "b"]).count)
练习 2:定义一个 kw_only dataclass,包含 title 和 urgent。
python
from dataclasses import dataclass
@dataclass(kw_only=True)
class Task:
title: str
urgent: bool = False
print(Task(title="Read", urgent=True))
4.13 对象协议进阶:可调用、索引与上下文 练习答案
练习 1 :实现 call,让对象把数字翻倍。
python
class Double:
def __call__(self, value):
return value * 2
print(Double()(3))
练习 2 :实现 contains,让对象支持成员测试。
python
class Tags:
def __init__(self, values):
self.values = values
def __contains__(self, item):
return item in self.values
print("a" in Tags(["a"]))
4.14 typing 进阶:Callable、TypeVar 与 overload 练习答案
练习 1 :定义一个接收 Callable[[int], int] 的高阶函数。
python
from typing import Callable
def apply(fn: Callable[[int], int], value: int) -> int:
return fn(value)
print(apply(abs, -4))
练习 2:用 TypeVar 编写返回列表首元素的泛型函数。
python
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
print(first([1, 2]))
4.15 泛型接口与依赖注入组合 练习答案
练习 1 :定义 Store[T] 协议,并提供内存实现。
python
from typing import Protocol, TypeVar
T = TypeVar("T")
class Store(Protocol[T]):
def save(self, value: T) -> None: ...
class Memory:
def __init__(self): self.values=[]
def save(self, value: int) -> None: self.values.append(value)
s=Memory(); s.save(1); print(s.values)
练习 2:写一个依赖 Clock 协议的服务。
python
class Clock:
def now(self): return 1
class Service:
def __init__(self, clock): self.clock=clock
def stamp(self): return self.clock.now()
print(Service(Clock()).stamp())
本卷验收
- 能解释本卷每个"本单元新语法"表中的形式。
- 能独立完成全部练习,并说明错误示例失败的原因。
- 能运行项目里程碑:
taskflow/milestones/v04_modeled_tasks.py --demo。 - 能说出下一卷开始前必须掌握的三个概念。