特殊方法把自定义对象接入 Python 的操作协议:obj()、len(obj)、下标访问、比较和迭代都有相应入口。重点是返回正确的结果并遵守协议,而不是给普通方法随意加双下划线。本文示例以 Python 3.11 为基线,每段都可单独保存并运行。
包文件与模块元数据,例如 __init__.py、__main__.py、__all__、__future__、__name__、__file__、__doc__、__package__、__loader__、__spec__、__qualname__、__annotations__、__module__、__bases__、__dict__,集中说明于包、模块与导入。其中 __qualname__ 不包含模块名,__spec__ 是导入信息对象,不是导入器本身。
按操作查找特殊方法
跳转到“按操作查找特殊方法”| 操作 | 相关名称 | 必须注意的行为 |
|---|---|---|
| 创建、初始化 | __new__、__init__ | 前者返回对象,后者初始化已有实例且应返回 None |
| 对象终结 | __del__ | 不保证在期望时刻或解释器退出时可靠执行 |
| 文本、字节、格式化 | __str__、__repr__、__bytes__、__format__ | 按协议分别返回字符串或字节串 |
| 比较 | __lt__、__le__、__eq__、__ne__、__gt__、__ge__ | 对不支持的类型通常返回 NotImplemented |
| 哈希、真值 | __hash__、__bool__ | 相等对象必须有相同哈希;__bool__ 返回布尔值 |
| 属性读写 | __getattribute__、__getattr__、__setattr__、__delattr__、__dir__ | 委托基础实现以避免递归;缺失属性应有明确边界 |
| 调用 | __call__ | 让实例接受 obj(...);参数是否匹配仍需检查 |
| 长度、下标 | __len__、__getitem__、__setitem__、__delitem__ | 切片也是传给下标方法的一种键 |
| 迭代、成员判断 | __iter__、__next__、__reversed__、__contains__ | 迭代器结束时抛出 StopIteration |
| 字典缺失键 | __missing__ | dict 子类的 d[key] 可触发;get() 不会触发 |
| pickle 重建与状态 | __reduce__、__reduce_ex__、__getnewargs__、__getstate__、__setstate__ | 重建参数、状态与协议版本是不同职责 |
| 实例属性布局 | __slots__ | 是类变量,限制特定布局下的新属性,不等于不可变 |
创建、表示与调用
跳转到“创建、表示与调用”下面把一个读数对象同时接入文本、字节、格式化、真值与调用协议。format(value, spec) 才是把格式规格用于数值的直接方式;spec.format(value) 含义不同。
import math
class Reading: def __new__(cls, value): if not math.isfinite(float(value)): raise ValueError("value must be finite") return super().__new__(cls)
def __init__(self, value): self.value = float(value)
def __repr__(self): return f"Reading({self.value!r})"
def __str__(self): return f"{self.value} V"
def __bytes__(self): return str(self).encode("utf-8")
def __format__(self, format_spec): return f"{format(self.value, format_spec)} V"
def __bool__(self): return bool(self.value)
def __call__(self, factor=1.0): return self.value * factor
reading = Reading(3.5)assert repr(reading) == "Reading(3.5)"assert str(reading) == "3.5 V"assert bytes(reading) == b"3.5 V"assert f"{reading:.2f}" == "3.50 V"assert reading(2) == 7.0 and reading() == 3.5assert callable(reading) and bool(reading)assert not Reading(0)try: Reading(float("nan"))except ValueError: passelse: raise AssertionError("non-finite reading was accepted")print("representation and call checks passed")这里的 __new__ 只是展示“创建前验证”的位置,普通类往往只需写 __init__。repr() 应尽量提供明确的调试表示,不强制要求每个对象的表示都能由 eval() 重建。__del__ 也不是 del variable 的直接回调:删除名称、对象不再可达与执行终结方法不能混为一谈。资源释放应使用明确的关闭操作或上下文管理。基础定义见 Python 数据模型。
比较和哈希必须一起设计
跳转到“比较和哈希必须一起设计”以下键对象只与相同类型比较。冻结字段使参与比较和哈希的值在正常属性赋值中保持稳定;total_ordering 根据 __eq__ 与 __lt__ 补齐其余排序操作。
from dataclasses import dataclass, FrozenInstanceErrorfrom functools import total_ordering
@total_ordering@dataclass(frozen=True, eq=False)class SampleKey: channel: int sample: int
def __eq__(self, other): if type(other) is not type(self): return NotImplemented return (self.channel, self.sample) == (other.channel, other.sample)
def __lt__(self, other): if type(other) is not type(self): return NotImplemented return (self.channel, self.sample) < (other.channel, other.sample)
def __hash__(self): return hash((type(self), self.channel, self.sample))
a, same, later = SampleKey(1, 2), SampleKey(1, 2), SampleKey(1, 3)assert a == same and hash(a) == hash(same)assert len({a, same, later}) == 2assert a < later and a <= same and later > a and later >= aassert a != later and a != (1, 2)assert a.__eq__((1, 2)) is NotImplementedtry: a < (1, 2)except TypeError: passelse: raise AssertionError("unsupported ordering was accepted")try: a.sample = 99except FrozenInstanceError: passelse: raise AssertionError("frozen field changed")print("comparison and hash checks passed")NotImplemented 是一个返回值,与抛出 NotImplementedError 不同。单写 __eq__ 而没有提供适当哈希的类,通常会变成不可哈希对象;可变对象若按会变化的内容计算哈希,就可能破坏字典和集合中的查找。排序方法之间也不会仅因定义了 __lt__ 就自动全部出现,示例的补齐来自 functools.total_ordering。冻结数据类的行为见 dataclasses 文档。
属性钩子要委托基础实现
跳转到“属性钩子要委托基础实现”普通实例属性读取先经 __getattribute__;正常查找以 AttributeError 失败后,点号或 getattr() 等访问才会尝试 __getattr__。下面只提供一个虚拟属性 double,其他缺失名称仍抛出 AttributeError,从而让 hasattr() 能正确报告不存在。
class Channel: def __init__(self, value): object.__setattr__(self, "_reads", []) self.value = value
def __getattribute__(self, name): if name != "_reads": object.__getattribute__(self, "_reads").append(name) return object.__getattribute__(self, name)
def __getattr__(self, name): if name == "double": return self.value * 2 raise AttributeError(name)
def __setattr__(self, name, value): if name == "value" and type(value) is not int: raise TypeError("value must be an int") object.__setattr__(self, name, value)
def __delattr__(self, name): if name == "value": raise AttributeError("value is required") object.__delattr__(self, name)
def __dir__(self): return sorted(set(super().__dir__()) | {"double"})
channel = Channel(3)assert channel.value == 3 and channel.double == 6assert "value" in channel._reads and "double" in dir(channel)assert not hasattr(channel, "unknown")channel.note = "temporary"del channel.noteassert not hasattr(channel, "note")for operation, expected in [ (lambda: setattr(channel, "value", "3"), TypeError), (lambda: delattr(channel, "value"), AttributeError),]: try: operation() except expected: pass else: raise AssertionError("invalid attribute operation succeeded")print("attribute hook checks passed")如果在 __setattr__ 中再次执行 setattr(self, ...),就会重新进入同一个钩子;__delattr__ 和 __getattribute__ 也有同样的递归风险。仅打印日志却没有返回查找结果的 __getattribute__ 会让正常属性读取得到 None。因此示例把实际读写明确交给 object。
容器与迭代器承担不同职责
跳转到“容器与迭代器承担不同职责”容器可以每次返回一个新的迭代器;迭代器本身通常让 iter(iterator) 返回自己。下面的 Bag 委托列表处理索引、切片和越界,Countdown 则保存迭代进度。
class Bag: def __init__(self, values): self.items = list(values)
def __len__(self): return len(self.items)
def __getitem__(self, key): return self.items[key]
def __setitem__(self, key, value): self.items[key] = value
def __delitem__(self, key): del self.items[key]
def __iter__(self): return iter(self.items)
def __reversed__(self): return reversed(self.items)
def __contains__(self, value): return value in self.items
class Countdown: def __init__(self, start): if type(start) is not int or start < 0: raise ValueError("start must be a non-negative int") self.remaining = start
def __iter__(self): return self
def __next__(self): if self.remaining == 0: raise StopIteration self.remaining -= 1 return self.remaining + 1
bag = Bag([10, 20, 30])assert len(bag) == 3 and bag[1:] == [20, 30]bag[1] = 25del bag[0]assert list(bag) == [25, 30] and list(reversed(bag)) == [30, 25]assert 25 in bag and 99 not in bagassert iter(bag) is not iter(bag)assert not Bag([])try: bag[99]except IndexError: passelse: raise AssertionError("out-of-range index was accepted")
iterator = Countdown(3)assert iter(iterator) is iteratorassert next(iterator) == 3 and list(iterator) == [2, 1]for _ in range(2): try: next(iterator) except StopIteration: pass else: raise AssertionError("exhausted iterator resumed")assert list(Countdown(0)) == []print("container and iterator checks passed")Bag 没有定义 __bool__,因此空容器通过长度为零表现为假。__len__ 本身应返回非负整数。__reversed__ 返回反向遍历的迭代器,不必返回原迭代器的“反向版本”。迭代终止约定见 Python 迭代器类型。
缺失键、特殊方法查找与 __slots__
跳转到“缺失键、特殊方法查找与 __slots__”下面三个短类分别展示容易混淆的边界:缺失字典键与普通属性不同;给单个实例添加 __len__ 不能使 len() 自动采用它;slots 限制新属性的前提是实例没有可用的属性字典。
class DefaultDict(dict): def __missing__(self, key): return "missing:" + str(key)
mapping = DefaultDict(existing=1)assert mapping["absent"] == "missing:absent"assert mapping.get("absent") is Noneassert "absent" not in mapping
class Plain: pass
plain = Plain()plain.__len__ = lambda: 9assert plain.__len__() == 9try: len(plain)except TypeError: passelse: raise AssertionError("instance hook unexpectedly implemented len")Plain.__len__ = lambda self: 4assert len(plain) == 4 and plain.__len__() == 9
class Slotted: __slots__ = ("value",)
def __init__(self, value): self.value = value
class FlexibleChild(Slotted): pass
class SlottedChild(Slotted): __slots__ = ()
for item in (Slotted(1), SlottedChild(1)): item.value = 2 assert item.value == 2 and not hasattr(item, "__dict__") try: item.extra = 3 except AttributeError: pass else: raise AssertionError("undeclared attribute was accepted")flexible = FlexibleChild(1)flexible.extra = 3assert flexible.__dict__ == {"extra": 3}print("missing key, lookup and slots checks passed")示例没有把缺失键插入字典;是否插入取决于 __missing__ 的实现。get() 的边界见 字典类型文档。隐式特殊方法查找通常绕过实例属性;需要改变对象参与语言操作的方式时,应在类上定义相应方法。slots 不阻止对已有槽赋新值,而没有继续声明 slots 的子类通常重新获得实例字典。
pickle:分别设计重建参数与状态
跳转到“pickle:分别设计重建参数与状态”以下示例只反序列化自己刚生成的字节。pickle 数据可触发代码执行,不能把不可信字节交给 loads()。类应定义在可导入模块的顶层,跨进程恢复时还必须能找到相同的模块和类;函数内部的局部类通常不能直接 pickle。
import pickle
class Snapshot: init_calls = 0
def __new__(cls, name): instance = super().__new__(cls) instance.name = name return instance
def __init__(self, name): type(self).init_calls += 1 self.values = [] self.cache = []
def __getnewargs__(self): return (self.name,)
def __getstate__(self): return {"version": 1, "values": self.values}
def __setstate__(self, state): if state["version"] != 1: raise ValueError("unsupported snapshot version") self.values = state["values"] self.cache = []
original = Snapshot("sensor-A")original.values.extend([1, 2, 3])original.cache.append("derived result")for protocol in range(2, pickle.HIGHEST_PROTOCOL + 1): restored = pickle.loads(pickle.dumps(original, protocol=protocol)) assert restored is not original assert restored.name == "sensor-A" and restored.values == [1, 2, 3] assert restored.cache == []assert Snapshot.init_calls == 1
class ReducedValue: def __init__(self, value): self.value = value
def __reduce__(self): return (type(self), (self.value,))
def __reduce_ex__(self, protocol): return self.__reduce__()
for protocol in range(pickle.HIGHEST_PROTOCOL + 1): value = ReducedValue(42) copy = pickle.loads(pickle.dumps(value, protocol=protocol)) assert type(copy) is ReducedValue and copy.value == 42print("pickle reconstruction checks passed")Snapshot 的 __getnewargs__ 给恢复阶段的 __new__ 提供位置参数;状态钩子保存业务数据并重新建立缓存。默认实例恢复过程不会像普通构造那样再次调用 __init__。如果 __new__ 需要仅限关键字参数,应考虑 __getnewargs_ex__。
ReducedValue 演示更底层的重建配方:返回“可调用对象、参数元组”,此例用类本身作可调用对象,所以恢复时会正常调用它的构造流程。pickle 优先采用 __reduce_ex__ 并传入协议版本;此类无需区分版本,才统一转交 __reduce__。不要把该简化配方直接用于所有带内部状态的对象。详见 pickle 的类实例与重建协议。