Python
类、协议与 Python 数据模型
用类保护领域不变量,以组合、协议和数据模型方法构建 Python 风格接口。
发布于 2026年7月23日
类、协议与 Python 数据模型
Python 的面向对象不仅是“把函数放进类”。类通过数据模型协议参与打印、比较、迭代、上下文管理等语言行为。好的类保护不变量,并保持公开接口小而明确。
一、学习目标
- 用类表达有行为的数据
- 理解实例属性、类属性和方法绑定
- 使用属性、组合与协议控制边界
- 实现常见数据模型方法
- 为任务建立不可变领域对象
二、建立任务类
from datetime import UTC, datetime
from enum import StrEnum
class TaskStatus(StrEnum):
TODO = "todo"
DONE = "done"
class Task:
def __init__(self, task_id: int, title: str) -> None:
clean_title = title.strip()
if not clean_title:
raise ValueError("任务标题不能为空")
self.id = task_id
self.title = clean_title
self.status = TaskStatus.TODO
self.created_at = datetime.now(UTC)
def complete(self) -> None:
self.status = TaskStatus.DONE
self 是调用方法时绑定的实例。task.complete() 大致相当于 Task.complete(task)。
三、实例属性与类属性
class Task:
max_title_length = 200
def __init__(self, title: str) -> None:
if len(title) > self.max_title_length:
raise ValueError("标题过长")
self.title = title
类属性适合所有实例共享的常量。不要把可变列表作为类属性保存每个实例的数据,否则实例会共享状态。
四、数据模型方法
class Task:
def __init__(self, task_id: int, title: str) -> None:
self.id = task_id
self.title = title
def __repr__(self) -> str:
return f"Task(task_id={self.id!r}, title={self.title!r})"
def __str__(self) -> str:
return f"#{self.id} {self.title}"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Task):
return NotImplemented
return self.id == other.id
repr面向开发和诊断,尽量明确、无歧义。str面向用户展示。- 比较不支持的类型时返回
NotImplemented,让 Python 尝试反向比较或得出正确结果。
如果定义值相等且对象需要作为字典键,还必须认真设计哈希与可变性。可变对象通常不应可哈希。
五、属性与不变量
class Task:
def __init__(self, title: str) -> None:
self.title = title
@property
def title(self) -> str:
return self._title
@title.setter
def title(self, value: str) -> None:
clean = value.strip()
if not clean:
raise ValueError("任务标题不能为空")
self._title = clean
属性让调用方保持 task.title 的自然语法,同时类可以维护不变量。不要为每个字段机械地写 getter/setter;只有确实需要验证、计算或兼容时再使用。
六、组合优先
任务服务依赖仓储:
class TaskService:
def __init__(self, repository: "TaskRepository") -> None:
self._repository = repository
def add(self, title: str) -> Task:
task = Task(0, title)
return self._repository.add(task)
TaskService 拥有一个仓储,而不是继承仓储。组合让业务规则和持久化策略可以独立变化。
继承适合真正的“是一个”关系以及稳定的扩展协议。为了复用几行代码建立深层继承树,通常会产生隐式耦合。
七、协议与鸭子类型
from typing import Protocol
class TaskRepository(Protocol):
def add(self, task: Task) -> Task: ...
def list(self) -> list[Task]: ...
class TaskService:
def __init__(self, repository: TaskRepository) -> None:
self._repository = repository
任何实现所需方法的对象都满足协议,不必显式继承。这种结构化子类型保留了 Python 鸭子类型的灵活性,同时让类型检查器验证接口。
八、类方法与静态方法
类方法常用于替代构造器:
class Task:
@classmethod
def from_row(cls, row: dict[str, object]) -> "Task":
return cls(task_id=int(row["id"]), title=str(row["title"]))
静态方法不接收实例或类,适合与类概念紧密相关的纯函数。但如果函数属于模块层更自然,就无需放进类。
九、常见错误
可变类属性
class Board:
tasks = []
所有 Board 实例共享同一列表。应在 __init__ 中创建实例属性。
滥用继承
“服务需要数据库”不代表 TaskService 应继承 Database。应注入依赖。
绕过对象不变量
如果任意代码都能把状态写成未知字符串,状态模型就没有价值。使用枚举和受控方法更新。
十、练习与自测
- 为
Task增加complete(),重复完成时保持幂等。 - 定义内存仓储,使其满足
TaskRepository协议。 - 实现清晰的
repr,观察 pytest 断言失败时的输出。
自测:
- 组合为什么通常比深继承更容易测试?
NotImplemented与抛出NotImplementedError有何不同?- 协议如何支持鸭子类型?
十一、官方资料
上一篇:模块、包、虚拟环境与依赖 | 下一篇:异常、上下文管理与资源安全