← 返回题目列表

Python 怎么重载运算符?__add__、__lt__ 和 total_ordering 怎么用?

中等 第 19 / 21 题 更新于 2026/07/31
Python运算符重载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 + ba.__add__(b)a < ba.__lt__(b)a == ba.__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 NotImplementedErrorNotImplemented 是一个「值」(运算符方法返回它)、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 < ba.__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 + ba.__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 selffunctools.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__(否则不可哈希);多字段用元组比较」。