Python
类型注解、dataclass 与结构化数据
使用类型注解、数据类、TypedDict 和 Protocol 表达领域模型与接口契约。
发布于 2026年7月23日
类型注解、dataclass 与结构化数据
类型注解不会自动把 Python 变成静态语言,也不会替代运行时校验。它的价值是让接口、领域模型和重构意图可以被编辑器、类型检查器和读者共同验证。
一、学习目标
- 为函数、容器和可空值编写注解
- 理解运行时对象与静态类型的边界
- 使用
dataclass表达任务实体 - 用
Protocol、TypedDict描述接口 - 理解 Python 3.14 注解延迟求值
二、函数和容器注解
def normalize_title(value: str, *, maximum: int = 200) -> str:
title = value.strip()
if not title:
raise ValueError("任务标题不能为空")
if len(title) > maximum:
raise ValueError("任务标题过长")
return title
def titles_by_id(tasks: list["Task"]) -> dict[int, str]:
return {task.id: task.title for task in tasks}
现代 Python 使用 list[str]、dict[str, int] 和 str | None。只有确实接受任意类型时才使用 Any,否则它会让检查在该位置失效。
三、类型注解不是输入校验
def double(value: int) -> int:
return value * 2
double("a") # 运行时得到 "aa"
解释器默认不会根据注解拒绝字符串。来自命令行、JSON、数据库和网络的数据仍需运行时解析与校验。类型注解描述的是完成验证后的内部契约。
四、使用 dataclass 建模
from dataclasses import dataclass
from datetime import UTC, datetime
from enum import StrEnum
class TaskStatus(StrEnum):
TODO = "todo"
DONE = "done"
@dataclass(frozen=True, slots=True)
class Task:
id: int
title: str
status: TaskStatus
created_at: datetime
updated_at: datetime
@classmethod
def new(cls, title: str) -> "Task":
clean = title.strip()
if not clean:
raise ValueError("任务标题不能为空")
now = datetime.now(UTC)
return cls(0, clean, TaskStatus.TODO, now, now)
frozen=True阻止普通属性重新赋值,使状态变化更明确。slots=True减少实例属性存储并阻止随意添加新属性。- 自动生成
repr和按字段比较。
“冻结”不代表深度不可变;字段内部若包含列表,列表仍可修改。
五、不可变更新
from dataclasses import replace
from datetime import UTC, datetime
completed = replace(
task,
status=TaskStatus.DONE,
updated_at=datetime.now(UTC),
)
返回新对象让状态变化更容易追踪,特别适合测试和并发读取。数据库层仍负责使用同一个主键更新持久化记录。
六、TypedDict 描述字典形状
from typing import TypedDict
class TaskPayload(TypedDict):
id: int
title: str
status: str
def serialize(task: Task) -> TaskPayload:
return {
"id": task.id,
"title": task.title,
"status": task.status.value,
}
TypedDict 只服务静态检查,运行时仍是普通字典。外部 JSON 必须检查键和类型后,才能安全地视为 TaskPayload。
七、Protocol 描述行为
from typing import Protocol
class TaskRepository(Protocol):
def add(self, task: Task) -> Task: ...
def list(self, *, status: TaskStatus | None = None) -> list[Task]: ...
服务只依赖行为协议,测试可以传入内存实现,生产使用 SQLite 实现。
class TaskService:
def __init__(self, repository: TaskRepository) -> None:
self._repository = repository
八、泛型
from collections.abc import Iterable
from typing import TypeVar
T = TypeVar("T")
def first_or_none(values: Iterable[T]) -> T | None:
return next(iter(values), None)
泛型保留输入和输出类型之间的关系。若使用 object,调用方只能得到 object | None;使用类型变量后,传入 Iterable[Task] 会得到 Task | None。
九、Python 3.14 的注解求值
Python 3.14 默认延迟求值注解,前向引用通常无需写成字符串,也减少定义时求值带来的导入问题。需要在运行时读取注解时,应使用官方支持的 annotationlib 或 typing.get_type_hints,不要直接依赖类命名空间中的内部表示。
静态类型检查与运行时注解反射是两个不同需求。大多数业务代码只需让检查器读取注解。
十、常见错误
- 为了让 mypy 安静而到处写
Any或# type: ignore。 - 认为
cast(Task, value)会在运行时转换或验证对象。 - 把数据库行、JSON 字典直接声明为已验证领域对象。
- 一个函数既可能返回值、
None、字符串错误,又可能抛异常。
十一、练习与自测
- 将字典任务替换为冻结的
Task数据类。 - 为仓储写协议,并实现内存版本。
- 编写解析函数,把未知 JSON 安全转换为
TaskPayload。
自测:
- 类型注解为什么不能替代运行时校验?
TypedDict与dataclass在运行时有什么区别?- Python 3.14 延迟注解对前向引用有何影响?
十二、官方资料
上一篇:迭代器、生成器与装饰器 | 下一篇:文件系统、序列化、正则与时间