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) 的结果。需要定制隐式操作时,扩展点在类上,而不在某个实例的同名属性中。
repr、str 与格式化
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()"
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__() 必须返回真正的 bool,0 和 1 即使能表示假与真,仍然是不合格的整数返回值,真值测试会因此抛出 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"
越界序列索引和缺失映射键应分别以 IndexError 与 KeyError 表达。若对所有错误索引静默返回哨兵值,迭代终止和调用方的错误处理都会失去这些协议信号。
可调用对象与转换协议
实例定义 __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__() 不会无界地再次调用自身。下一章将把这些容器与下标规则延伸到可迭代对象、迭代器、生成器与推导式。