Python

Python 中关于"相等比较"的那些事

Python 中关于"相等比较"的那些事

在 Python 里判断两个值"相等"看似简单,一个 == 就够了,但背后其实涉及值比较与身份比较的区别、自定义对象的协议、哈希一致性、浮点数精度等一系列容易踩坑的细节。本文梳理这些要点,帮助写出更可靠的比较逻辑。

1. ==is:值相等 vs 身份相等

== 比较的是内容(调用 __eq__ 方法),is 比较的是身份,也就是两个变量是否指向内存中同一个对象,等价于 id(a) == id(b)

a = [1, 2, 3]
b = [1, 2, 3]
c = a

a == b   # True,内容相同
a is b   # False,不同对象
a is c   # True,同一个对象

判断变量是否为 None 时应使用 is None 而非 == NoneNone 在整个程序中是唯一的单例,is 语义更准确,且不会触发自定义的 __eq__ 方法,效率也更高。

2. 自定义对象的相等性

默认情况下,自定义类的实例比较退化为 is(即基于地址)。要让对象支持按值比较,需要重写 __eq__

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

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

类型不匹配时返回 NotImplemented(而不是直接 False)是惯例:Python 会据此尝试调用对方的 __eq__,避免过早给出错误的比较结果。

3. __eq____hash__ 必须保持一致

只重写 __eq__ 而不重写 __hash__,Python 会自动把该类标记为不可哈希,实例无法放入 set 或作为 dict 的键。规则是:两个对象 == 相等,它们的 hash() 值就必须相等(反之不要求)。因此通常需要同时实现:

def __hash__(self):
    return hash((self.x, self.y))

4. 浮点数比较的精度陷阱

浮点数的二进制表示存在精度误差,直接用 == 往往不可靠:

0.1 + 0.2 == 0.3              # False
math.isclose(0.1 + 0.2, 0.3)  # True,推荐做法

涉及浮点运算的相等判断,应优先使用 math.isclose() 并根据场景设置合适的相对/绝对误差容限。

5. NaN 的特殊性

根据 IEEE 754 标准,nan 不等于任何值,包括自身:

float('nan') == float('nan')   # False
math.isnan(float('nan'))       # True,正确的判断方式

6. 容器是"深度"比较

list、tuple、dict、set 等容器用 == 时会递归比较元素内容,而不是比较容器自身的地址:

[1, [2, 3]] == [1, [2, 3]]   # True
{1: 'a'} == {1: 'a'}          # True

这意味着即便两个容器是不同对象(isFalse),只要元素逐一相等,== 就会成立。

7. 链式比较

a < b < c 等价于 a < b and b < c,中间表达式只会被求值一次,写法简洁但要注意涉及函数调用时的求值顺序。

8. 小整数/字符串缓存:is 的常见误区

CPython 会缓存小整数(通常 -5 到 256)和部分字符串(驻留机制),导致 1 is 1 之类的表达式"恰好"为 True。这是实现细节,不可依赖,判断值是否相等永远应该用 ==,is 只用于身份判断(尤其是和 None、单例对象比较)

9. 用 functools.total_ordering 简化排序类比较

只需实现 __eq____lt__,再加上装饰器即可自动补全 __le____gt____ge__

from functools import total_ordering

@total_ordering
class Point:
    def __eq__(self, other): ...
    def __lt__(self, other): ...

小结清单

  • 判断值是否相同 → ==;判断是否为同一对象(尤其 None)→ is
  • 自定义类按值比较 → 重写 __eq__,并同步实现 __hash__
  • 浮点数比较 → 用 math.isclose,避免直接 ==
  • NaN 判断 → 用 math.isnan,不要用 ==
  • 容器比较是递归的深度比较,而非地址比较。
  • 不要依赖小整数/字符串缓存产生的 is 结果做逻辑判断。
  • 需要排序协议时,搭配 functools.total_ordering 减少重复代码。