浏览知识库目录

Python

类、协议与 Python 数据模型

用类保护领域不变量,以组合、协议和数据模型方法构建 Python 风格接口。

类、协议与 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。应注入依赖。

绕过对象不变量

如果任意代码都能把状态写成未知字符串,状态模型就没有价值。使用枚举和受控方法更新。


十、练习与自测

  1. Task 增加 complete(),重复完成时保持幂等。
  2. 定义内存仓储,使其满足 TaskRepository 协议。
  3. 实现清晰的 repr,观察 pytest 断言失败时的输出。

自测:

  • 组合为什么通常比深继承更容易测试?
  • NotImplemented 与抛出 NotImplementedError 有何不同?
  • 协议如何支持鸭子类型?

十一、官方资料

上一篇:模块、包、虚拟环境与依赖 | 下一篇:异常、上下文管理与资源安全