2. 比较操作符重载:__eq__、__ne__、__lt__、__le__、__gt__、__ge__ 的实现与实战

比较操作符,说白了就是 ==、!=、<、<=、>、>= 这六个符号。Python 默认拿它们比较的是对象的内存地址,而不是你关心的业务逻辑。

举个例子,你定义了一个 Person 类,有两个属性相同的实例。用 == 一比较,返回 False。为什么?因为 Python 在问:「这两个对象是同一个内存地址吗?」而不是问:「这两个人的名字和年龄一样吗?」

嗯,这就是我们今天要解决的问题。

核心认知: 比较操作符重载的本质,是告诉 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__,剩下的四个就是简单的逻辑组合。

比较操作符重载:核心推导逻辑 __eq__ __lt__ __ne__ __le__ __gt__ __ge__ not self.__eq__() self.__lt__() or self.__eq__() not self.__le__() not self.__lt__() 只需实现 __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__

掌握了这些,你的对象就能优雅地参与排序、去重、集合运算了。代码写出来,自己看着都舒服。