核心篇 · Core
lv.2 核心
kp-019
魔术方法(dunder protocols)
1. 一句话定义
__xxx__ 形式的魔术方法是语言协议的挂载点:+ 会调 __add__、len() 调 __len__、for 调 __iter__——Python 的所有内置行为都派发给这些方法,自定义类通过实现它们接入语言。
2. 为什么重要
这是 Python 数据模型的全部秘密:语法是协议的外壳。掌握核心 dunder 集合,就能让自定义对象像原生类型一样 +、切片、比较、上下文管理;这是从"会用库"到"能写库"的分水岭。
3. 前置知识
4. 核心概念(按用途分组)
| 协议 | 魔术方法 | 派发的语法 |
|---|---|---|
| 表示 | __repr__ __str__ | REPL/调试、print() |
| 相等与哈希 | __eq__ __hash__ | ==、dict 键/set 成员 |
| 迭代 | __iter__ __next__ | for、解包、推导式 |
| 序列 | __len__ __getitem__ __contains__ | len()、obj[i]、in |
| 算术 | __add__ __sub__ __mul__(及 __radd__ 反射版) | + - * |
| 比较 | __lt__ __le__ …(或用 functools.total_ordering) | < <= |
| 调用 | __call__ | obj() |
| 上下文 | __enter__ __exit__ | with(kp-025) |
| 布尔 | __bool__(缺省用 __len__) | if 真值判断 |
5. 原理与机制
python
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self): # 调试友好的表示
return f"Vector({self.x!r}, {self.y!r})"
def __eq__(self, other):
if not isinstance(other, Vector):
return NotImplemented # 让 Python 尝试 other 方向/回退
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y)) # 与 __eq__ 保持一致!
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __len__(self):
return 2
def __getitem__(self, i):
return (self.x, self.y)[i] # 有了它,切片、迭代都"免费"获得
v1, v2 = Vector(1, 2), Vector(3, 4)
v1 + v2 # Vector(4, 6)
len(v1), v1[0] # 2, 1
{v1, v2, Vector(1,2)} # set 去重成立(__eq__/__hash__ 联动)规则:__eq__ 定义后必须同步定义 __hash__(否则默认可哈希性失效);__repr__ 面向开发者(必写),__str__ 缺省回退到 __repr__。
6. 关键事实(模型/图示)
text
语法糖 → 派发表:
a + b → a.__add__(b)(失败试 b.__radd__(a))
a == b → a.__eq__(b)(NotImplemented 则试反向/恒等)
in → __contains__(缺省退化到 __iter__ 扫描)
for → __iter__(缺省退化到 __getitem__ 整数下标)
鸭子类型:实现的不是类型而是协议7. 直观类比
魔术方法像插头插座标准:语法(+、for、with)是插座,你的类是电器——只要插头符合标准(实现协议),任何插座都能用。你不需要继承"插座父类",符合形状即可——这就是鸭子类型的工程化表达。
8. 实例与案例
python
# 让自定义对象可排序:只写 __lt__,其余比较借用装饰器
from functools import total_ordering
@total_ordering
class Student:
def __init__(self, name, score): ...
def __eq__(self, o): return self.score == o.score
def __lt__(self, o): return self.score < o.score9. 常见误区
__eq__与__hash__不一致 —— 相等的对象哈希必须相同,否则 dict/set 行为错乱。- 在
__eq__里直接返回 False 而不是 NotImplemented —— 断绝了反向比较与其他类型协作的可能。 __str__写得花哨、__repr__却没写 —— 调试(REPL、日志、容器内打印)看的是__repr__。- 在算术方法里原地修改 self ——
__add__应返回新对象;原地语义交给__iadd__。
10. 自测题
v1 + v2底层发生了什么(含 v1 是 str 时的反射分支)?- 定义了
__eq__而不定义__hash__,对象还能进 set 吗? - 只实现
__getitem__,for x in obj能工作吗?为什么?
参考答案
- 先调
v1.__add__(v2);若返回 NotImplemented 则试v2.__radd__(v1);都失败抛 TypeError。 - 不能(可哈希性被置 None);进 set 会报 unhashable type。
- 能;旧式序列协议会退化为
obj[0], obj[1], ...的下标迭代直到 IndexError。
11. 与其他知识点的关系
12. 延伸阅读
- Python 官方文档 Data Model(数据模型章是全语言最重要的一章)
- Fluent Python 第 1、11 章