Python 类、数据模型与类型标注
# Python 类、数据模型与类型标注
理解类、实例、组合、继承和 Python 数据模型,并使用类型标注表达稳定边界,而不是把所有代码都改写成类。
# 先记住一句话
类把状态与行为组织在一起;类型标注描述代码契约;两者都服务于可读性和边界,而不是越多越好。
# 实例、类属性和方法
# dataclass 用于自动生成 __init__、__repr__ 和 __eq__ 等样板方法。
# field 用于单独配置某个属性,例如为每个实例创建独立的默认列表。
from dataclasses import dataclass, field
# @dataclass 是类装饰器语法,可近似理解为先定义类,再执行 Note = dataclass(Note)。
# 它会加工下面的 Note 类;slots=True 限制随意增加属性,并可减少部分内存开销。
@dataclass(slots=True)
class Note:
"""表示一篇笔记的数据;dataclass 自动生成初始化和比较等样板方法。"""
# 这些类型标注也会成为自动生成的 __init__ 方法参数。
title: str
content: str
# 不能直接写 tags=[];default_factory=list 会为每个 Note 实例创建新列表。
tags: list[str] = field(default_factory=list)
def word_count(self) -> int:
"""按空白粗略统计英文单词;中文分词需要专门工具。"""
# self 表示当前 Note 实例;split() 按空白拆分,len() 统计结果数量。
return len(self.content.split())
# 根据 dataclass 自动生成的 __init__ 创建 Note 实例,tags 使用默认空列表。
note = Note(title="Python", content="learn step by step")
# 修改当前实例自己的 tags,不会影响其他 Note 实例。
note.tags.append("backend")
# 调用实例方法,输出 4。
print(note.word_count())
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
default_factory=list 会为每个实例创建独立列表,避免实例之间共享一个可变默认值。slots=True 可以限制任意新增实例属性并减少部分内存开销,但不是所有类都必须使用。
# Python 数据模型
len(obj)、for item in obj 和 obj[key] 看似是不同语法,背后都通过对象实现的特殊方法工作,例如 __len__、__iter__ 和 __getitem__。
# Note 是上文定义的数据类;NoteCollection 用来统一管理多个 Note 实例。
class NoteCollection:
# 创建 NoteCollection 时自动调用 __init__;-> None 表示它不返回业务结果。
def __init__(self, notes: list[Note]) -> None:
# list(notes) 复制外层列表,避免调用方随后增删原列表时悄悄改变集合。
self._notes = list(notes)
# 实现 __len__ 后,len(collection) 会自动调用这个方法。
def __len__(self) -> int:
return len(self._notes)
# 实现 __iter__ 后,for 循环会从这里取得迭代器并逐个读取 Note。
def __iter__(self):
return iter(self._notes)
# 将上文创建的 note 放入列表,再创建一个笔记集合。
collection = NoteCollection([note])
# 触发 __len__,输出集合中的笔记数量 1。
print(len(collection))
# 触发 __iter__,依次取得每个 Note 实例。
for item in collection:
print(item.title)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这就是 Python 的协议思想:对象不必继承某个庞大基类,只要实现约定的方法,就能参与对应语法。
# 组合优先于继承
from typing import Protocol
# Protocol 用来描述对象必须具备的成员,主要供静态类型检查器判断。
class NoteStore(Protocol):
"""任何提供 get 方法的对象都可作为笔记存储。"""
# ... 是 Ellipsis 占位符:这里只声明方法契约,不提供具体实现。
def get(self, note_id: str) -> Note | None: ...
class NoteService:
def __init__(self, store: NoteStore) -> None:
# 服务依赖抽象行为,不依赖某个具体数据库实现。
self._store = store
def get_title(self, note_id: str) -> str:
note = self._store.get(note_id)
if note is None:
raise LookupError(f"笔记不存在:{note_id}")
return note.title
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Protocol 使用结构化类型:对象只要具备所需方法,静态检查器就认为它满足协议,不要求显式继承。这和 Go 的接口思想很接近。
# 类型标注能做什么
# abc 是 Abstract Base Classes(抽象基类)的缩写,是标准库中的固定模块。
# Iterable 表示可被 for 遍历的对象;TypeVar 用于声明泛型类型变量。
from collections.abc import Iterable
from typing import TypeVar
# T 是类型占位符:传入元素是什么类型,正常返回值就保持什么类型。
T = TypeVar("T")
# T | None 表示可能返回一个 T 类型元素,也可能在空集合时返回 None。
def first(items: Iterable[T]) -> T | None:
"""返回任意可迭代对象的首项,保持输入元素类型。"""
# iter() 创建迭代器;next(..., None) 取首项,没有元素时返回默认值 None。
return next(iter(items), None)
2
3
4
5
6
7
8
9
10
11
12
13
类型标注可以表达参数、返回值、容器元素、可选值和泛型关系,但 CPython 默认不会在运行时逐项校验。工程中常用静态检查器发现错误,再用 Pydantic 等工具校验不可信的外部数据。
`Any` 不是 `object`
Any 相当于让静态检查器放弃检查,错误可以继续传播;object 表示值可以是任意对象,但使用前仍要缩小类型。边界不确定时优先考虑 object 或明确联合类型,不要到处使用 Any。
# 高频面试题与回答
1. Python 的鸭子类型是什么?参考答案
鸭子类型关心对象能做什么,而不是它名义上属于哪个类。只要对象提供调用方需要的方法,就可以被使用。类型标注中的 Protocol 能把这种隐含约定显式表达出来。
2. 类型标注会让 Python 变成静态类型语言吗?参考答案
不会。类型标注主要供开发工具和静态检查器使用,默认不改变运行时的动态类型行为。外部请求仍要做运行时校验,单元测试也不能被类型检查替代。
3. 实例方法、类方法和静态方法有什么区别?参考答案
实例方法接收 self 并操作具体对象;类方法用 @classmethod 接收 cls,常用于替代构造器或类级配置;静态方法不自动接收实例或类,只是放在类命名空间中的相关函数。若逻辑与类状态无关,模块级函数往往更直接。
4. Protocol 和抽象基类怎样选择?参考答案
Protocol 主要表达结构化子类型,只要对象具有所需成员,静态检查就可以认为它满足协议;抽象基类通过显式继承表达名义关系,还可以提供共享实现和运行时约束。调用方只依赖少量行为时 Protocol 更松耦合,需要统一继承契约时考虑抽象基类。
5. dataclass 解决了什么问题?参考答案
@dataclass 根据字段声明生成初始化、比较和展示等常见样板代码,适合以数据为主的值对象。它不会自动完成外部输入校验,也不会让对象天然不可变;需要只读意图时可使用 frozen,需要 API 校验时仍应使用专门的校验模型。
# 接下来学什么
下一篇学习 迭代、生成器与上下文管理,理解 Python 如何按需处理数据并可靠释放资源。