2. 比较操作符重载:__eq__、__ne__、__lt__、__le__、__gt__、__ge__ 的实现与实战
比较操作符,说白了就是 ==、!=、<、<=、>、>= 这六个符号。Python 默认拿它们比较的是对象的内存地址,而不是你关心的业务逻辑。
举个例子,你定义了一个 Person 类,有两个属性相同的实例。用 == 一比较,返回 False。为什么?因为 Python 在问:「这两个对象是同一个内存地址吗?」而不是问:「这两个人的名字和年龄一样吗?」
嗯,这就是我们今天要解决的问题。
2.1 六兄弟的对应关系
先记住这张表,我建议你把它贴在显示器旁边。我在项目中见过太多人把 __lt__ 和 __gt__ 的实现搞反了。
| 操作符 | 魔术方法 | 含义 |
|---|---|---|
== |
__eq__(self, other) |
相等 |
!= |
__ne__(self, other) |
不相等 |
< |
__lt__(self, other) |
小于 |
<= |
__le__(self, other) |
小于等于 |
> |
__gt__(self, other) |
大于 |
>= |
__ge__(self, other) |
大于等于 |
每个方法都接收两个参数:self 和 other。你只需要返回 True 或 False。
2.2 从零开始:实现一个 Person 类
我们直接上手。假设你在做一个员工管理系统,需要比较两个人的工龄和薪资等级。
class Person:
def __init__(self, name, age, salary_level):
self.name = name
self.age = age
self.salary_level = salary_level # 1-10 级
def __eq__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age
def __ne__(self, other):
if not isinstance(other, Person):
return NotImplemented
return not self.__eq__(other)
def __lt__(self, other):
if not isinstance(other, Person):
return NotImplemented
# 先按薪资等级排,再按年龄排
if self.salary_level != other.salary_level:
return self.salary_level < other.salary_level
return self.age < other.age
def __le__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.__lt__(other) or self.__eq__(other)
def __gt__(self, other):
if not isinstance(other, Person):
return NotImplemented
return not self.__le__(other)
def __ge__(self, other):
if not isinstance(other, Person):
return NotImplemented
return not self.__lt__(other)
def __repr__(self):
return f"Person({self.name}, {self.age}, Lv.{self.salary_level})"
__eq__ 和 __lt__,然后用逻辑推导出其他四个。这样代码量少一半,也不容易出错。
测试一下:
p1 = Person("张三", 28, 5)
p2 = Person("李四", 32, 5)
p3 = Person("张三", 28, 5)
print(p1 == p3) # True
print(p1 != p2) # True
print(p1 < p2) # True(同级,年龄小)
print(p1 > p2) # False
print(p1 <= p3) # True(相等)
2.3 避坑指南:NotImplemented 不是 NotImplementedError
我曾经在一个项目中看到同事这样写:
def __eq__(self, other):
if not isinstance(other, Person):
raise NotImplementedError("不支持与非Person对象比较")
这是个大坑。你应该返回 NotImplemented 这个单例对象,而不是抛出异常。为什么?
NotImplemented告诉 Python:「我不知道怎么比,你问问other那边有没有办法。」NotImplementedError直接炸掉程序,不给对方任何机会。
Python 看到 NotImplemented 后,会尝试调用 other 的反射方法(比如 __eq__ 的反射是 __eq__,__lt__ 的反射是 __gt__)。如果两边都说「不知道」,最后才抛出 TypeError。
raise NotImplementedError 来处理类型不匹配的情况。返回 NotImplemented 才是 Python 的规范做法。
2.4 知识体系:比较操作符的核心逻辑
下面这张图展示了六个方法之间的推导关系。你想想看,只要掌握了 __eq__ 和 __lt__,剩下的四个就是简单的逻辑组合。
2.5 实战:让对象可排序
实现了比较操作符后,你的对象就能直接用于 sorted()、max()、min() 等函数了。我个人觉得这是最爽的地方。
people = [
Person("王五", 35, 7),
Person("赵六", 29, 4),
Person("张三", 28, 5),
Person("李四", 32, 5),
]
sorted_people = sorted(people)
print(sorted_people)
# 输出:[Person(赵六, 29, Lv.4), Person(张三, 28, Lv.5), Person(李四, 32, Lv.5), Person(王五, 35, Lv.7)]
你看,排序规则完全按照我们定义的 __lt__ 来:先按薪资等级升序,同级按年龄升序。
2.6 进阶:functools.total_ordering 装饰器
如果你觉得手动推导六个方法太麻烦,Python 标准库提供了一个偷懒神器。
from functools import total_ordering
@total_ordering
class Person:
def __init__(self, name, age, salary_level):
self.name = name
self.age = age
self.salary_level = salary_level
def __eq__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age
def __lt__(self, other):
if not isinstance(other, Person):
return NotImplemented
if self.salary_level != other.salary_level:
return self.salary_level < other.salary_level
return self.age < other.age
def __repr__(self):
return f"Person({self.name}, {self.age}, Lv.{self.salary_level})"
加上 @total_ordering 后,你只需要实现 __eq__ 和 __lt__,Python 会自动补全剩下的四个方法。
@total_ordering 省事。但如果是性能敏感的场景,比如每秒要比较几百万次,手动实现全部六个方法能省掉装饰器的反射开销。
2.7 常见陷阱总结
- 类型检查不能省: 不检查
isinstance,万一传进来一个字符串,你的self.salary_level < other.salary_level直接炸掉。 - 返回
NotImplemented而不是False: 返回False会掩盖「无法比较」这个事实,导致诡异的 bug。 - 一致性要保证: 如果
a < b为True,那么b > a也必须为True。我曾经见过有人把__lt__和__gt__写成两套逻辑,结果排序结果时对时错,排查了一下午。 - 哈希方法要配套: 如果你重写了
__eq__,记得把对象放进集合或作为字典键时,也要重写__hash__。这个我们后面章节会细讲。
__eq__ 写成了比较商品 ID,但 __hash__ 没改。结果同一个商品在购物车集合里出现了两次,用户下单时被扣了双倍钱。嗯,从那以后我每次重写 __eq__ 都会检查 __hash__。
2.8 小结
比较操作符重载,说白了就是给你的对象装上「比较能力」。你只需要记住:
- 六个方法对应六个操作符
- 核心实现
__eq__和__lt__,其余四个可以推导 - 类型不匹配时返回
NotImplemented - 重写
__eq__时别忘了__hash__
掌握了这些,你的对象就能优雅地参与排序、去重、集合运算了。代码写出来,自己看着都舒服。