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 而非 == None:None 在整个程序中是唯一的单例,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这意味着即便两个容器是不同对象(is 为 False),只要元素逐一相等,== 就会成立。
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减少重复代码。