>>>PyPathPython 学习站
首页›核心篇›kp-019
核心篇 · Core lv.2 核心 kp-019

魔术方法(dunder protocols)

前置知识:kp-017、kp-012

1. 一句话定义

__xxx__ 形式的魔术方法是语言协议的挂载点:+ 会调 __add__、len() 调 __len__、for 调 __iter__——Python 的所有内置行为都派发给这些方法,自定义类通过实现它们接入语言。

2. 为什么重要

这是 Python 数据模型的全部秘密:语法是协议的外壳。掌握核心 dunder 集合,就能让自定义对象像原生类型一样 +、切片、比较、上下文管理;这是从"会用库"到"能写库"的分水岭。

3. 前置知识

kp-012(迭代协议)、kp-017(类)。

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.score

9. 常见误区

  1. __eq__ 与 __hash__ 不一致 —— 相等的对象哈希必须相同,否则 dict/set 行为错乱。
  2. 在 __eq__ 里直接返回 False 而不是 NotImplemented —— 断绝了反向比较与其他类型协作的可能。
  3. __str__ 写得花哨、__repr__ 却没写 —— 调试(REPL、日志、容器内打印)看的是 __repr__。
  4. 在算术方法里原地修改 self —— __add__ 应返回新对象;原地语义交给 __iadd__。

10. 自测题

  1. v1 + v2 底层发生了什么(含 v1 是 str 时的反射分支)?
  2. 定义了 __eq__ 而不定义 __hash__,对象还能进 set 吗?
  3. 只实现 __getitem__,for x in obj 能工作吗?为什么?
参考答案
  1. 先调 v1.__add__(v2);若返回 NotImplemented 则试 v2.__radd__(v1);都失败抛 TypeError。
  2. 不能(可哈希性被置 None);进 set 会报 unhashable type。
  3. 能;旧式序列协议会退化为 obj[0], obj[1], ... 的下标迭代直到 IndexError。

11. 与其他知识点的关系

  • kp-027 元编程:__getattr__/描述符是协议的动态层。
  • kp-025 上下文管理器:with 协议专文。
  • kp-005 运算符:每个运算符的派发终点。

12. 延伸阅读

  • Python 官方文档 Data Model(数据模型章是全语言最重要的一章)
  • Fluent Python 第 1、11 章