引言:打破“魔法”的迷信

初学者看到 __init____str__ 这种双下划线方法就头大,只能死记硬背。但对于C++程序员来说,这些其实就是编译器在特定时刻自动调用的“钩子函数”。

核心类比

  • Python 的 obj + obj ≈ C++ 的 operator+ 重载。
  • Python 的 str(obj) ≈ C++ 的 operator std::string()toString() 虚函数。
  • Python 的 cls ≈ C++ 的 static 类作用域。

变身术:类型转换协议 (str vs int)

__str__ (人类视图)

  • C++ 类比:相当于 C++ 中重载 std::ostream& operator<<(std::ostream&, const T&)
  • 触发时机:当你执行 print(obj)str(obj) 时。
  • 用途:返回一个人类可读的字符串。

__repr__ (机器视图)

  • C++ 类比:相当于调试器中显示对象的逻辑。
  • 触发时机:当你在交互式终端中直接输入对象名并回车时。
  • 用途:返回一个能让 eval() 重建对象的字符串。

算术与增量运算协议

基本运算

  • __add__:对应 + 运算符。
  • __sub__:对应 - 运算符。
  • __mul__:对应 * 运算符。
  • __truediv__:对应 / 运算符。

反向运算

  • __radd__:当 a + ba 不支持 + 操作时,会尝试 b.__radd__(a)

增量运算

  • __iadd__:对应 += 运算符,比 + 更高效。

比较运算协议

  • __eq__:对应 == 运算符。
  • __lt__:对应 < 运算符。
  • __gt__:对应 > 运算符。
  • __le__:对应 <= 运算符。
  • __ge__:对应 >= 运算符。
  • __ne__:对应 != 运算符。

技巧

Python 有个 @total_ordering 装饰器,只要你定义了 __eq__ 和一个比较符(如 __lt__),它会自动补全其他的(类似 C++ 的 std::rel_ops)。

容器与布尔协议:Python 特有的“真值测试”

__len__

  • C++ 类比:相当于类的 .size().length() 成员函数。
  • 触发时机:当你调用 len(obj) 时。

__bool__

  • C++ 类比:相当于 C++ 的 explicit operator bool()
  • 触发时机:当你执行 if obj: 时。
  • 注意:如果没有定义这个,Python 会退而求其次看 __len__(如果长度为 0 则为 False)。

容器协议

  • __getitem__:对应 obj[key] 读取操作。
  • __setitem__:对应 obj[key] = value 赋值操作。
  • __iter__:让对象变成可迭代对象。
  • __next__:实现迭代器协议。

代码实战:Vector 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from functools import total_ordering

@total_ordering
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

def __repr__(self):
return f"Vector({self.x}, {self.y})"

def __str__(self):
return f"({self.x}, {self.y})"

def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented

def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

def __rmul__(self, scalar):
return self * scalar

def __eq__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return self.x == other.x and self.y == other.y

def __lt__(self, other):
if not isinstance(other, Vector):
return NotImplemented
return abs(self) < abs(other)

def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5

def __len__(self):
# 向量的维度
return 2

def __bool__(self):
# 零向量为 False
return bool(abs(self))

# 测试
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1) # 输出: (3, 4)
print(v1 + v2) # 输出: Vector(4, 6)
print(v1 * 2) # 输出: Vector(6, 8)
print(2 * v1) # 输出: Vector(6, 8)
print(abs(v1)) # 输出: 5.0
print(v1 > v2) # 输出: True
print(bool(Vector(0, 0))) # 输出: False

总结:从“语法糖”到“协议”

C++ 的操作符重载是静态绑定的(编译期决定),而 Python 的魔术方法是动态派发的(运行时查找)。

不要死记硬背这些方法名,而是记住场景:

  • 想打印?找 __str__
  • 想相加?找 __add__
  • 想比大小?找 __lt__/__gt__/__eq__
  • 想变类型?找 __int__/__bool__

一句话心法:“你想让你的对象在什么场景下做什么事,就实现对应的魔术方法。”