跳到主要内容

Python 数据模型与特殊方法

本节目标

查询 Python 对象协议、特殊方法查找、表示、比较、哈希、运算和容器模拟。

数据模型把内置语法和对象操作连接起来:表达式、内置函数和运算符会按约定调用类型上定义的特殊方法。本章以 Python 3.14 数据模型参考内置类型参考为边界;对象身份、类型和值的基础可先参阅对象、名称、赋值与基本类型

对象、类型与协议

每个对象都有身份、类型和值;类型决定它支持的操作。协议是这类操作的约定:例如 len(value) 要求类型提供长度语义,通常通过 __len__() 返回非负整数。实现协议是让对象参与现有语法,而不是继承某个固定接口。

class Labels:
def __len__(self) -> int:
return 2

assert len(Labels()) == 2

__len__() 的返回值必须是非负整数;非整数会使 len() 抛出 TypeError,负数则会引发 ValueError。只有满足这一合同的对象才应对外提供长度语义。

特殊方法的隐式查找

特殊方法的隐式查找通常从对象的类型开始;len(value)value + other 这类操作不会像普通 value.method 那样依赖实例字典中的同名属性。应在类上定义 __len____add__ 等方法,让解释器按类型和运算规则分派。

class Count:
def __len__(self) -> int:
return 1

assert len(Count()) == 1

给单个实例赋值 value.__len__ = ... 不会可靠地改变 len(value) 的结果。需要定制隐式操作时,扩展点在类上,而不在某个实例的同名属性中。

reprstr 与格式化

repr(value) 调用 __repr__(),应给调试和开发者一个明确表示;str(value) 调用 __str__(),未定义时回退到 __repr__()format(value, spec) 和 f-string 的格式说明调用 __format__(spec);方法应返回 str

class Mark:
def __repr__(self) -> str:
return "Mark()"

assert repr(Mark()) == "Mark()"
data_model_report.py
from dataclasses import dataclass
from typing import overload


@dataclass(frozen=True)
class Temperature:
celsius: float

def __repr__(self) -> str:
return f"Temperature(celsius={self.celsius:.1f})"

def __str__(self) -> str:
return f"{self.celsius:.1f} °C"

def __format__(self, format_spec: str) -> str:
if format_spec == "C":
return f"{self.celsius:.1f}C"
return format(str(self), format_spec)

def __bool__(self) -> bool:
return self.celsius > -273.15


class CourseSequence:
def __init__(self, names: tuple[str, ...]) -> None:
self._names = names

def __len__(self) -> int:
return len(self._names)

def __contains__(self, name: object) -> bool:
return name in self._names

@overload
def __getitem__(self, index: int) -> str:
...

@overload
def __getitem__(self, index: slice) -> tuple[str, ...]:
...

def __getitem__(self, index: int | slice) -> str | tuple[str, ...]:
return self._names[index]


temperature = Temperature(23.5)
same_temperature = Temperature(23.5)
courses = CourseSequence(("Python", "Typing"))

print(f"repr={temperature!r}")
print(f"str={temperature}")
print(f"format={temperature:C}")
print(f"truthy={bool(temperature)}")
print(f"length={len(courses)}")
print(f"contains-python={'Python' in courses}")
print(f"item-0={courses[0]}")
print(f"equal-hash={temperature == same_temperature and hash(temperature) == hash(same_temperature)}")
repr=Temperature(celsius=23.5)
str=23.5 °C
format=23.5C
truthy=True
length=2
contains-python=True
item-0=Python
equal-hash=True

三个表示协议都承诺返回字符串;任一方法返回其他类型,对应操作就会抛出 TypeError。若表示还要用于测试或日志对比,应排除可变地址和其他运行时偶然值。

真值、长度与成员测试

bool(value) 优先调用 __bool__() 并要求其返回 bool;没有它时才使用 __len__(),长度为零即假。item in container 优先调用 __contains__(item),其结果按真值规则解释;缺少它时,解释器可退回迭代或旧式下标访问。

class Choices:
def __contains__(self, item: object) -> bool:
return item == "Python"

assert "Python" in Choices()

__bool__() 必须返回真正的 bool01 即使能表示假与真,仍然是不合格的整数返回值,真值测试会因此抛出 TypeError

富比较与 NotImplemented

__lt____le____eq____ne____gt____ge__ 接收另一个操作数;不能处理该组合时应返回单例 NotImplemented,让解释器尝试反射方法或其他回退。若比较最终没有实现,==!= 会按身份回退,而排序比较通常抛出 TypeError。Python 3.14 中对 NotImplemented 做真值测试会抛出 TypeError

class Token:
def __eq__(self, other: object) -> object:
return NotImplemented

assert Token() != Token()

检查这个哨兵值时使用 result is NotImplemented。例如 if NotImplemented: 这样的真值分支不再是可用写法,Python 3.14 会对其抛出 TypeError

相等性与哈希合同

__eq__() 定义值相等;可哈希对象的 __hash__() 返回整数并服务于字典和集合。相等的对象必须产生相同的哈希值,因此重写值相等时应让哈希基于同一不可变值,或明确令 __hash__ = None 使可变对象不可哈希。

class Pair:
def __hash__(self) -> int:
return hash((1, 2))

assert isinstance(hash(Pair()), int)

哈希值适合作为当前运行中的哈希容器输入,不能充当跨进程持久标识。字符串和字节串等的哈希可被随机化,因此确定性输出不应记录其具体数字。

算术、反向运算与原地运算

left + right 先尝试左侧类型的 __add__,必要时再尝试右侧的 __radd__;右侧是左侧类型的严格子类时,反射方法可优先。left += right 优先尝试 __iadd__,若它返回 NotImplemented 或不存在,则回退到普通加法;这些方法应返回运算结果。

class Score:
def __add__(self, other: object) -> object:
return NotImplemented

assert Score().__add__(1) is NotImplemented

NotImplemented 通知解释器继续尝试反射方法或其他分派,不是一个可供应用代码保存的运算结果。两侧均不支持该组合时,最终可观察的结果是 TypeError

容器模拟与下标访问

value[key] 调用 __getitem__(key);序列通常接受整数和 slice,映射通常接受键。__setitem____delitem__ 支持赋值和删除,__iter__ 或连续从零开始的 __getitem__ 可提供迭代回退。访问不存在的序列位置应抛出 IndexError,不存在的映射键应抛出 KeyError

class First:
def __getitem__(self, index: int) -> str:
if index != 0:
raise IndexError(index)
return "Python"

assert First()[0] == "Python"

越界序列索引和缺失映射键应分别以 IndexErrorKeyError 表达。若对所有错误索引静默返回哨兵值,迭代终止和调用方的错误处理都会失去这些协议信号。

可调用对象与转换协议

实例定义 __call__() 后可由 value(*args, **kwargs) 调用,返回值就是该方法的返回值。数值转换使用各自协议:int(value) 优先 __int__(),需要下标整数时使用 __index__()str(value) 使用 __str__(),而 bytes(value) 依其输入类别调用相应转换。

class Greeting:
def __call__(self, name: str) -> str:
return f"Hello, {name}"

assert Greeting()("Python") == "Hello, Python"

只有实现 __call__() 的实例才能被调用,否则调用表达式会抛出 TypeError。各转换方法也必须交付协议承诺的类型,外观相似的其他对象不能代替。

协议组合、递归表示与能力检查

先从内置操作反查协议,再检查返回值和回退规则:repr/str/format 都必须给出字符串,比较或算术不支持时返回 NotImplemented,可哈希的值对象保持相等—哈希合同,容器访问使用准确异常。若对象可通过属性回到自身,__repr__() 还应避免无界递归。这样既能复用 Python 的分派,也不会把实现偶然性写成接口。

class Unhashable:
__hash__ = None

assert not hasattr(Unhashable(), "__hash__") or Unhashable.__hash__ is None

“能比较”不代表同时可排序、可哈希或可下标;能力应通过对应协议逐项确认。自引用对象还要保证 __repr__() 不会无界地再次调用自身。下一章将把这些容器与下标规则延伸到可迭代对象、迭代器、生成器与推导式