Python 怎么重载运算符?__add__、__lt__ 和 total_ordering 怎么用?
简化版
Python 通过实现特定的「魔术方法(dunder method)」来「重载运算符」——让自定义对象支持 +、-、*、<、== 等运算符。每个运算符对应一个方法:① 算术:__add__(+)、__sub__(-)、__mul__(*)、__truediv__(/)、__floordiv__(//)、__mod__(%)、__pow__(**);② 比较:__lt__(<)、__le__(<=)、__gt__(>)、__ge__(>=)、__eq__(==)、__ne__(!=);③ 反射版:__radd__ 等(当左操作数不支持时,Python 尝试右操作数的反射方法,如 1 + obj 会调 obj.__radd__(1));④ 就地版:__iadd__ 等(+=)。functools.total_ordering 是个好帮手——只要你实现了 __eq__ 和「一个」比较方法(如 __lt__),它自动补全其余的比较方法(<=/>/>=),省去写四个的重复。关键点:比较方法应返回布尔或 NotImplemented(不支持时返回 NotImplemented 让 Python 尝试对方的方法)。核心记忆:运算符重载靠魔术方法(__add__/__lt__ 等),反射版 __radd__ 处理左操作数不支持的情况,@total_ordering 只写 __eq__ + 一个比较方法就自动补全其余。
详细版
常用运算符对应的魔术方法:
| 运算符 | 方法 | 反射版 | 就地版 |
|---|---|---|---|
+ | __add__ | __radd__ | __iadd__ |
- | __sub__ | __rsub__ | __isub__ |
* | __mul__ | __rmul__ | __imul__ |
< <= | __lt__ __le__ | — | — |
> >= | __gt__ __ge__ | — | — |
== != | __eq__ __ne__ | — | — |
from functools import total_ordering
# 算术运算符重载
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar): # v * 3
return Vector(self.x * scalar, self.y * scalar)
def __rmul__(self, scalar): # 3 * v(左操作数是 int,不支持)
return self.__mul__(scalar)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1, v2 = Vector(1, 2), Vector(3, 4)
print(v1 + v2) # Vector(4, 6)(__add__)
print(v1 * 3) # Vector(3, 6)(__mul__)
print(3 * v1) # Vector(3, 6)(__rmul__,因为 int.__mul__(Vector) 失败)
# 比较运算符 + total_ordering
@total_ordering # 自动补全其余比较方法
class Temperature:
def __init__(self, degrees):
self.degrees = degrees
def __eq__(self, other): # 必须实现 ==
return self.degrees == other.degrees
def __lt__(self, other): # 只需再实现一个(<)
return self.degrees < other.degrees
# __le__, __gt__, __ge__ 由 total_ordering 自动生成!
t1, t2 = Temperature(20), Temperature(30)
print(t1 < t2) # True(__lt__)
print(t1 <= t2) # True(total_ordering 生成)
print(t1 > t2) # False(total_ordering 生成)
print(sorted([t2, t1])) # [Temperature(20), Temperature(30)](能排序)
# 返回 NotImplemented(不支持时让 Python 尝试对方)
class Money:
def __init__(self, amount): self.amount = amount
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented # 不支持 → 让 Python 试 other.__radd__
return Money(self.amount + other.amount)
# 就地运算符 __iadd__(+=)
class Counter:
def __init__(self, n=0): self.n = n
def __iadd__(self, x): # counter += 5
self.n += x
return self # 必须返回 self(或新对象)
⚠️ 核心机制:Python 的运算符不是硬编码的——
a + b实际是「先尝试a.__add__(b),如果它返回NotImplemented(或 a 没有__add__),再尝试b.__radd__(a)」;比较运算符类似。这套「反射(reflected)+ NotImplemented 回退」机制让运算符能优雅处理「两个不同类型」的运算:比如3 * vector——int.__mul__(vector)不知道怎么处理 Vector、返回 NotImplemented,Python 于是尝试vector.__rmul__(3)(这就是为什么标量在左边也能乘)。所以正确的重载要点:不支持的类型返回NotImplemented(不是抛异常、也不是返回 False),让 Python 有机会尝试对方的反射方法或最终抛 TypeError。关于比较:手写全部六个比较方法(</<=/>/>=/==/!=)既繁琐又易错(容易不一致),functools.total_ordering装饰器让你只写__eq__和其中一个排序方法(如__lt__),它自动推导补全其余——代价是自动生成的方法性能略低(多一层调用),性能敏感时可手写全部。另外__eq__和__hash__要配套:定义了__eq__会导致__hash__被设为 None(对象变不可哈希),需要能当字典键就要一并定义__hash__。
完整版教学
一、运算符靠魔术方法实现
先理解运算符和魔术方法的对应:
Python 运算符 = 调用对应的魔术方法
a + b → a.__add__(b)
a - b → a.__sub__(b)
a * b → a.__mul__(b)
a < b → a.__lt__(b)
a == b → a.__eq__(b)
a[i] → a.__getitem__(i)
len(a) → a.__len__()
重载 = 在自己的类里实现这些方法
class MyNum:
def __add__(self, other):
return ... # 定义 + 的行为
算术运算符与方法:
+ __add__ - __sub__ * __mul__
/ __truediv__ // __floordiv__ % __mod__
** __pow__ -x __neg__ +x __pos__
abs(x) __abs__
比较运算符与方法:
< __lt__ <= __le__ > __gt__
>= __ge__ == __eq__ != __ne__
位运算:
& __and__ | __or__ ^ __xor__
<< __lshift__ >> __rshift__ ~ __invert__
为什么重载运算符:
让自定义类型"用起来像内置类型"
Vector(1,2) + Vector(3,4) 比 v1.add(v2) 自然
→ 数学对象(向量、矩阵、复数、分数、货币)尤其受益
所以运算符=调用魔术方法(a+b→a.__add__(b)),重载=在类里实现这些方法
Python 运算符 = 调用对应的魔术方法:a + b → a.__add__(b)、a < b → a.__lt__(b)、a == b → a.__eq__(b)、a[i] → a.__getitem__(i)、len(a) → a.__len__()。重载 = 在自己的类里实现这些方法。算术运算符与方法:+(__add__)、-(__sub__)、*(__mul__)、/(__truediv__)、//(__floordiv__)、%(__mod__)、**(__pow__)、-x(__neg__)、abs(__abs__)。比较运算符与方法:<(__lt__)、<=(__le__)、>(__gt__)、>=(__ge__)、==(__eq__)、!=(__ne__)。位运算:&(__and__)、|(__or__)、^(__xor__)、<<(__lshift__)、~(__invert__)。为什么重载运算符:让自定义类型「用起来像内置类型」(Vector(1,2) + Vector(3,4) 比 v1.add(v2) 自然、数学对象尤其受益)。理解「运算符=调用魔术方法(a+b→a.add(b));重载=类里实现;算术+/-/*/add/sub/mul、比较</==/lt/eq、位运算&/|;让自定义类型像内置类型(向量/货币)」,就掌握了运算符对应的魔术方法。
二、反射方法与 NotImplemented
理解运算符的回退机制:
a + b 的完整流程(反射机制):
① 先尝试 a.__add__(b)
- 返回结果 → 用它
- 返回 NotImplemented → 下一步
- a 没有 __add__ → 下一步
② 尝试 b.__radd__(a)(反射版,reflected)
- 返回结果 → 用它
- 返回 NotImplemented / 没有 → 下一步
③ 都不行 → 抛 TypeError
反射方法(__r*__):处理"左操作数不支持"的情况
3 * vector # int.__mul__(vector) 不认识 Vector → 返回 NotImplemented
# → 尝试 vector.__rmul__(3) → 成功
→ 所以标量在左边也能乘(靠 __rmul__)
常见反射:__radd__ __rsub__ __rmul__ __rtruediv__ ...
返回 NotImplemented(关键!):
不支持的类型要返回 NotImplemented(不是抛异常、不是 False)
def __add__(self, other):
if not isinstance(other, MyType):
return NotImplemented # 让 Python 尝试对方或抛 TypeError
return ...
→ 返回 NotImplemented 让"反射机制"有机会工作
NotImplemented vs NotImplementedError:
NotImplemented:一个"值"(单例),运算符方法返回它
NotImplementedError:一个"异常",抽象方法里 raise 它
→ 别混!运算符重载返回 NotImplemented(值)
就地运算符(__iadd__ 等):
a += b → 先试 a.__iadd__(b),没有则退回 a = a.__add__(b)
__iadd__ 应"原地修改并 return self"(可变对象)
→ 不可变对象不定义 __iadd__,用 __add__ 生成新对象
所以 a+b 先试__add__、NotImplemented 则试对方__radd__;不支持返回 NotImplemented(不是异常)
a + b 的完整流程(反射机制):① 先尝试 a.__add__(b)(返回结果用它、返回 NotImplemented 或没有则下一步)、② 尝试 b.__radd__(a)(反射版)、③ 都不行抛 TypeError。反射方法(__r*__):处理「左操作数不支持」的情况(3 * vector——int.__mul__(vector) 返回 NotImplemented → 尝试 vector.__rmul__(3) 成功、所以标量在左边也能乘)。返回 NotImplemented(关键!):不支持的类型要返回 NotImplemented(不是抛异常、不是 False),让「反射机制」有机会工作。NotImplemented vs NotImplementedError:NotImplemented 是一个「值」(运算符方法返回它)、NotImplementedError 是一个「异常」(抽象方法里 raise、别混)。就地运算符(__iadd__):a += b 先试 a.__iadd__(b)、没有则退回 a = a.__add__(b),__iadd__ 应「原地修改并 return self」(可变对象)。理解「a+b 先试 a.add、返回 NotImplemented 则试 b.radd、都不行抛 TypeError;反射版处理左操作数不支持(3*vector 靠__rmul__);不支持返回 NotImplemented(值,不是异常/False);__iadd__原地改 return self」,就掌握了反射机制。
三、比较运算符与 eq
理解比较方法和相等:
六个比较运算符与方法:
< __lt__(less than)
<= __le__(less or equal)
> __gt__(greater than)
>= __ge__(greater or equal)
== __eq__(equal)
!= __ne__(not equal,默认由 __eq__ 取反)
比较方法的返回:
① 布尔(True/False)
② NotImplemented(不支持该类型的比较时)
def __lt__(self, other):
if not isinstance(other, MyType):
return NotImplemented
return self.key < other.key
反射(比较也有):
a < b 若 a.__lt__(b) 返回 NotImplemented
→ 尝试 b.__gt__(a)(< 的反射是 >)
== 的反射是 == 自己
__eq__ 与 __hash__ 配套(重要坑):
定义 __eq__ 后,__hash__ 会被自动设为 None
→ 对象变"不可哈希"(不能当字典键/放集合)
class P:
def __eq__(self, other): ...
# 现在 hash(P()) 报 TypeError!
解决:
① 也定义 __hash__(要和 __eq__ 一致:相等的对象 hash 相同)
def __hash__(self): return hash(self.key)
② 若对象可变、不该当键 → 保持不可哈希(合理)
为什么 == 默认比身份:
不定义 __eq__ 时,== 默认是 is(比对象身份)
→ 定义 __eq__ 才能"按值相等"
所以比较方法__lt__/__eq__等返回布尔或 NotImplemented;定义__eq__后要配__hash__(否则不可哈希)
六个比较运算符与方法:<(__lt__)、<=(__le__)、>(__gt__)、>=(__ge__)、==(__eq__)、!=(__ne__,默认由 __eq__ 取反)。比较方法的返回:布尔(True/False)或 NotImplemented(不支持该类型比较时)。反射(比较也有):a < b 若 a.__lt__(b) 返回 NotImplemented → 尝试 b.__gt__(a)(< 的反射是 >)、== 的反射是 == 自己。__eq__ 与 __hash__ 配套(重要坑):定义 __eq__ 后 __hash__ 会被自动设为 None、对象变「不可哈希」(不能当字典键/放集合),解决:① 也定义 __hash__(要和 __eq__ 一致:相等的对象 hash 相同)、② 若对象可变不该当键则保持不可哈希(合理)。为什么 == 默认比身份:不定义 __eq__ 时 == 默认是 is(比对象身份)、定义 __eq__ 才能「按值相等」。理解「六个比较方法__lt__/le/gt/ge/eq/ne;返回布尔或 NotImplemented;比较也有反射(< 的反射是>);★定义__eq__后__hash__变 None 对象不可哈希、要配套定义__hash__(相等对象 hash 相同);==默认比身份、定义__eq__才按值」,就掌握了比较运算符。
四、functools.total_ordering
理解 total_ordering 自动补全比较方法:
问题:手写全部六个比较方法繁琐且易错
要写 __lt__ __le__ __gt__ __ge__ __eq__ __ne__
→ 逻辑重复、容易不一致(如 < 和 >= 逻辑对不上)
functools.total_ordering:自动补全比较方法
只需实现:
① __eq__(必须)
② __lt__ / __le__ / __gt__ / __ge__ 中的"一个"
→ 装饰器自动推导补全其余的
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor):
self.major, self.minor = major, minor
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
# __le__, __gt__, __ge__ 自动生成
v1, v2 = Version(1, 0), Version(1, 5)
v1 < v2, v1 <= v2, v1 > v2, v1 >= v2 # 全部可用
自动推导逻辑(举例):
有 __lt__ 和 __eq__ →
__gt__ = not (< or ==)
__le__ = (< or ==)
__ge__ = not <
代价:
自动生成的方法性能略低(多一层组合调用)
→ 性能敏感的排序热路径可手写全部
小技巧——用元组比较:
__lt__/__eq__ 里把字段组成元组比较
(self.a, self.b) < (other.a, other.b)
→ 元组按字典序逐项比较,一行搞定多字段排序
所以@total_ordering:只写__eq__+一个比较方法,自动补全其余;代价性能略低
问题:手写全部六个比较方法繁琐且易错(逻辑重复、容易不一致如 < 和 >= 对不上)。functools.total_ordering:自动补全比较方法——只需实现 ① __eq__(必须)、② __lt__/__le__/__gt__/__ge__ 中的「一个」,装饰器自动推导补全其余(@total_ordering class Version: def __eq__; def __lt__ 后 <=/>/>= 全部可用)。自动推导逻辑(举例):有 __lt__ 和 __eq__ → __gt__ = not (< or ==)、__le__ = (< or ==)、__ge__ = not <。代价:自动生成的方法性能略低(多一层组合调用、性能敏感的排序热路径可手写全部)。小技巧——用元组比较:__lt__/__eq__ 里把字段组成元组比较((self.a, self.b) < (other.a, other.b)、元组按字典序逐项比较、一行搞定多字段排序)。理解「@total_ordering 只写__eq__+一个比较方法(如__lt__)自动补全其余(<=/>/>=);推导 gt=not(<or==)等;代价性能略低(热路径手写);技巧用元组比较多字段((a,b)<(c,d))」,就掌握了 total_ordering。
五、实践注意与陷阱
理解运算符重载的常见坑:
陷阱1:__eq__ 导致对象不可哈希
定义 __eq__ → __hash__ 变 None → 不能当字典键
解决:需要可哈希就定义 __hash__(和 __eq__ 一致)
或用 @dataclass(frozen=True)(自动生成 __eq__ + __hash__)
陷阱2:忘了返回 NotImplemented
✗ def __add__(self, other):
return MyType(self.v + other.v) # other 不是 MyType 会崩
✓ def __add__(self, other):
if not isinstance(other, MyType):
return NotImplemented
return ...
→ 不支持的类型返回 NotImplemented,别抛异常/返回错误结果
陷阱3:__iadd__ 忘了 return self
def __iadd__(self, x):
self.n += x
return self # 必须返回!否则 a += b 后 a 变 None
陷阱4:比较逻辑不一致(不用 total_ordering 时)
手写六个方法容易 < 和 >= 逻辑对不上
→ 用 total_ordering 或用元组比较保证一致
陷阱5:== 默认是身份比较
不定义 __eq__,== 就是 is(两个"值相同"的对象不相等)
→ 要按值相等必须定义 __eq__
最佳实践:
① 数学/值对象(向量、货币、版本)→ 重载运算符很值得
② 用 @dataclass 自动生成 __eq__(省手写)
③ 排序用 @total_ordering(只写 __eq__ + __lt__)
④ 不支持的类型返回 NotImplemented
⑤ __eq__ 和 __hash__ 配套
⑥ 别过度重载(语义不清晰的别硬套运算符)
所以陷阱:__eq__导致不可哈希、忘返回 NotImplemented、__iadd__忘 return self、比较不一致
运算符重载的常见坑:陷阱1:__eq__ 导致对象不可哈希(定义 __eq__ → __hash__ 变 None → 不能当字典键,解决定义 __hash__ 或用 @dataclass(frozen=True));陷阱2:忘了返回 NotImplemented(不支持的类型返回 NotImplemented 别抛异常/返回错误结果);陷阱3:__iadd__ 忘了 return self(否则 a += b 后 a 变 None);陷阱4:比较逻辑不一致(不用 total_ordering 时)(手写六个方法容易对不上、用 total_ordering 或元组比较);陷阱5:== 默认是身份比较(不定义 __eq__ 时 == 是 is、要按值相等必须定义)。最佳实践:数学/值对象重载值得、用 @dataclass 自动生成 __eq__、排序用 @total_ordering、不支持的类型返回 NotImplemented、__eq__ 和 __hash__ 配套、别过度重载。理解「陷阱:eq__导致不可哈希(配__hash)、忘返回 NotImplemented、iadd__忘 return self、比较不一致(用 total_ordering)、==默认比身份;最佳实践 dataclass 生成__eq、排序用 total_ordering、别过度重载」,就掌握了实践注意。
六、总结与实践
总结运算符重载:
核心:
运算符 = 调用魔术方法(a + b → a.__add__(b))
重载 = 在类里实现这些方法
算术:__add__/__sub__/__mul__/__truediv__/__mod__/__pow__
比较:__lt__/__le__/__gt__/__ge__/__eq__/__ne__
位运算:__and__/__or__/__xor__/__lshift__
反射:__radd__ 等(左操作数不支持时试右操作数)
就地:__iadd__ 等(+=,要 return self)
反射机制:
a + b:先 a.__add__(b),返回 NotImplemented 则试 b.__radd__(a)
→ 不支持的类型返回 NotImplemented(不是异常/False)
比较 + total_ordering:
@total_ordering:只写 __eq__ + 一个比较方法,自动补全其余
→ 省去手写六个的重复和不一致风险
关键配套:
__eq__ 定义后 __hash__ 变 None(对象不可哈希)
→ 需要可哈希就定义 __hash__(和 __eq__ 一致)
实践建议:
① 数学/值对象重载运算符(自然直观)
② @dataclass 自动生成 __eq__、@total_ordering 补全比较
③ 不支持的类型返回 NotImplemented
④ __iadd__ 记得 return self
⑤ 别过度重载(语义不清别硬套)
核心总结:
运算符重载靠魔术方法(__add__/__lt__)
反射版 __radd__ 处理左操作数不支持
@total_ordering 只写 __eq__ + 一个比较方法自动补全
所以运算符重载靠魔术方法,反射版处理左不支持,@total_ordering 自动补全比较
核心:运算符 = 调用魔术方法、重载 = 在类里实现。算术 __add__/__sub__/__mul__ 等、比较 __lt__/__eq__ 等、位运算、反射 __radd__(左操作数不支持时试右)、就地 __iadd__(+=、要 return self)。反射机制:a + b 先 a.__add__(b)、返回 NotImplemented 则试 b.__radd__(a)(不支持的类型返回 NotImplemented)。比较 + total_ordering:@total_ordering 只写 __eq__ + 一个比较方法、自动补全其余。关键配套:__eq__ 定义后 __hash__ 变 None(需要可哈希就定义 __hash__)。理解「运算符重载靠魔术方法(add/lt);反射版__radd__处理左操作数不支持;@total_ordering 只写__eq__+一个比较方法自动补全;eq__配__hash;dataclass 生成__eq__、别过度重载」,就掌握了总结与实践。
记忆钩子:「Python 通过实现『魔术方法(dunder)』重载运算符:算术__add__(+)/sub(-)/mul(*)/truediv(/)/mod(%)/pow()、比较__lt__(<)/le(<=)/gt(>)/ge(>=)/eq(==)/ne(!=)、位运算__and__/or;★反射机制:a+b 先尝试 a.add(b),若返回 NotImplemented(或 a 没有__add__)再尝试 b.radd(a),都不行才抛 TypeError——所以 3*vector 能工作(int.mul__不认识 Vector 返回 NotImplemented→调 vector.rmul(3)),★不支持的类型要返回 NotImplemented(是个『值』、不是抛异常也不是返回 False,别和 NotImplementedError 异常混);就地版__iadd(+=)要原地改并 return self;★functools.total_ordering 是好帮手:只需实现__eq__和『一个』比较方法(如__lt__),它自动补全其余的<=/>/>=(省去写六个的重复和不一致风险,代价性能略低);★重要坑:定义__eq__后__hash__会被自动设为 None、对象变『不可哈希』(不能当字典键/放集合),需要可哈希就要一并定义__hash__(相等的对象 hash 必须相同),或用@dataclass 自动生成;技巧:lt/__eq__里把字段组成元组比较((self.a,self.b)<(other.a,other.b))一行搞定多字段排序」**。
七、常见误区与追问
- 误区:运算符不支持某个类型时,应该在魔术方法里抛异常。 不对——应该返回
NotImplemented(一个特殊的单例值,不是异常):def __add__(self, other): if not isinstance(other, MyType): return NotImplemented;这样 Python 会去尝试对方操作数的反射方法(如other.__radd__),如果也不行才由 Python 自动抛 TypeError;如果你直接抛异常,就切断了反射机制、让本可以工作的其他类型 + 你的对象也失败;注意NotImplemented(值)和NotImplementedError(异常)是两个不同的东西,运算符重载返回前者。 - 误区:3 * my_vector 无法工作,因为 int 不知道怎么乘 Vector。 能工作——靠反射方法:
3 * my_vector先尝试int.__mul__(my_vector),int 不认识 Vector、返回 NotImplemented,于是 Python 尝试my_vector.__rmul__(3)(反射版乘法),只要你的 Vector 类定义了__rmul__就能成功;这就是为什么标量在左边(3 * v)和在右边(v * 3)都能工作——右边靠__mul__、左边靠__rmul__;所以处理「和内置类型混合运算」时要记得实现反射方法。 - 误区:定义了 eq 之后,对象还能正常当字典键。 不能(除非再定义 hash)——Python 规定:一旦你在类里定义了
__eq__,这个类的__hash__会被自动设为 None、对象变成「不可哈希」,不能放进集合、不能当字典键(hash(obj)会抛 TypeError);这是为了保证「相等的对象必须有相同的 hash」这个约束不被破坏;解决办法:如果对象需要可哈希,就一并定义__hash__(且要和__eq__一致——相等的对象返回相同的 hash,通常return hash((self.a, self.b)));如果对象是可变的、本就不该当键,保持不可哈希是合理的。 - 误区:要支持排序,必须手动实现全部六个比较方法。 不用——用
functools.total_ordering装饰器,你只需实现__eq__和「四个排序方法中的一个」(通常__lt__),它会自动推导补全其余的(__le__/__gt__/__ge__);这既省去了写六个方法的重复劳动,也避免了手写时逻辑不一致的风险(比如<和>=对不上);代价是自动生成的方法性能略低(多一层组合调用),在排序热路径对性能极敏感时才需要手写全部。 - 追问:a + b 这个运算符表达式,Python 底层是怎么求值的? 分几步(反射机制):① 先尝试调用
a.__add__(b)——如果 a 有__add__且它返回了一个有效结果(不是 NotImplemented),就用这个结果;② 如果a.__add__返回NotImplemented(表示「我不知道怎么和 b 相加」)或者 a 根本没有__add__,Python 就尝试b.__radd__(a)(b 的反射加法);③ 如果b.__radd__也返回 NotImplemented 或不存在,Python 抛TypeError: unsupported operand type(s);有个特殊情况:如果 b 的类型是 a 的类型的子类、且子类重写了__radd__,Python 会优先尝试子类的__radd__;这套机制让「两个不同类型相加」能优雅协商——每一方都有机会说「我不会」,从而让另一方处理或最终报错;比较运算符(<等)也遵循类似的反射流程(a < b失败时尝试b > a)。 - 追问:functools.total_ordering 是怎么工作的,用它有什么代价?
total_ordering是一个类装饰器:它检查你的类实现了哪一个排序方法(__lt__/__le__/__gt__/__ge__),然后基于这一个方法加上__eq__自动「推导」并补全其余三个排序方法——比如你实现了__lt__和__eq__,它就用逻辑组合生成__gt__(= 既不小于也不等于)、__le__(= 小于或等于)、__ge__(= 不小于);你只需保证__eq__和那个排序方法逻辑正确、一致,其余的它保证正确一致;代价是:自动生成的方法是「组合调用」(比如__ge__内部会调__lt__再取反),比手写的直接实现多一两层函数调用、性能略低;所以对绝大多数场景(业务对象排序)用 total_ordering 很划算(省代码、保一致),只有在「大量元素频繁比较的性能热路径」才考虑手写全部六个方法。 - 追问:给自定义类实现比较和相等时,有什么最佳实践? 几条:① 相等用
__eq__——不定义时==默认是身份比较(is),要「按值相等」必须定义__eq__;实现时对不支持的类型返回 NotImplemented;② 排序用@total_ordering+__eq__+__lt__——省去写全部六个、保证一致;③ 用「元组比较」简化多字段——__lt__里return (self.a, self.b, self.c) < (other.a, other.b, other.c),元组按字典序逐项比较,一行处理多字段的优先级排序;④__eq__和__hash__配套——定义__eq__后对象变不可哈希,若需要当字典键/放集合就定义__hash__(return hash((self.a, self.b)),且必须满足「相等对象 hash 相同」);⑤ 能用@dataclass就用——@dataclass自动生成__eq__(@dataclass(order=True)还生成比较方法、frozen=True生成__hash__),省掉大量样板;⑥ 不支持的类型统一返回 NotImplemented、别抛异常;⑦ 别过度重载——只对「运算符语义清晰自然」的类型(数学对象、值对象)重载,语义不清的别硬套运算符(降低可读性)。
八、加强记忆
Python 通过实现「魔术方法(dunder)」重载运算符:算术 __add__(+)/__sub__(-)/__mul__(*)/__truediv__(/)/__mod__(%)/__pow__(**)、比较 __lt__(<)/__le__(<=)/__gt__(>)/__ge__(>=)/__eq__(==)/__ne__(!=)、位运算 __and__/__or__。反射机制:a + b 先尝试 a.__add__(b),若返回 NotImplemented(或 a 没有 __add__)再尝试 b.__radd__(a),都不行才抛 TypeError——所以 3 * vector 能工作(int.__mul__ 不认识 Vector 返回 NotImplemented → 调 vector.__rmul__(3)),不支持的类型要返回 NotImplemented(是个「值」、不是抛异常也不是返回 False,别和 NotImplementedError 异常混)。就地版 __iadd__(+=)要原地改并 return self。functools.total_ordering 是好帮手:只需实现 __eq__ 和「一个」比较方法(如 __lt__),它自动补全其余的 <=/>/>=(省去写六个的重复和不一致风险,代价性能略低)。重要坑:定义 __eq__ 后 __hash__ 会被自动设为 None、对象变「不可哈希」(不能当字典键/放集合),需要可哈希就要一并定义 __hash__(相等的对象 hash 必须相同),或用 @dataclass 自动生成。技巧:__lt__/__eq__ 里把字段组成元组比较((self.a, self.b) < (other.a, other.b))一行搞定多字段排序。一句话「运算符重载靠魔术方法(add/lt);反射版__radd__处理左操作数不支持(3*vector)、不支持返回 NotImplemented;@total_ordering 只写__eq__+一个比较方法自动补全;定义__eq__后要配__hash__(否则不可哈希);多字段用元组比较」。