eq 和 hash 必须同步修改:若重写 eq 但未定义 hash__,对象变为不可哈希;若支持哈希,需显式定义一致的 __hash__;若含可变字段,应保持 __hash 为 none。

__eq__ 和 __hash__ 必须同步修改
Python 对象默认用 id() 做比较和哈希,一旦你重写了 __eq__ 却没动 __hash__,对象就自动变成不可哈希的——放进 set 或当 dict 键时直接报 TypeError: unhashable type。
常见错误是只改 __eq__,以为“相等逻辑对了就行”。但 Python 规定:如果两个对象 __eq__ 返回 True,它们的 __hash__ **必须相同**;反之不成立。违反就破坏字典/集合底层逻辑。
- 若逻辑上支持哈希(比如所有用于比较的字段都不可变),显式定义
__hash__ = lambda self: hash((self.a, self.b)) - 若用了可变字段(如
list、dict)参与比较,别写__hash__,让它是None——这是安全的默认行为 - 别写
__hash__ = None,那会彻底禁用哈希;也不要用object.__hash__,它仍基于id,和你的__eq__冲突
__lt__ 等比较方法不能只补一个
只实现 __lt__ 并不能让 a 或 <code>sorted() 正常工作。Python 不会自动推导其他关系,sorted() 默认调用 __lt__,但 a 实际调用的是 <code>__le__,不存在就回退到 __gt__ 取反——结果往往错得离谱。
真正省事又可靠的做法是用 @functools.total_ordering 装饰器:
立即学习“Python免费学习笔记(深入)”;
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor):
self.major = major
self.minor = 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__,前提是至少定义了 __eq__ 和其中一个大小关系(如 __lt__)。
dataclass 的 eq=True 是表层糖,别当真
@dataclass(eq=True) 自动生成 __eq__,看起来省事,但它只按字段值逐个比较,不处理嵌套可变对象、浮点精度、NaN、或业务上的“逻辑相等”(比如忽略空格、大小写)。
典型翻车场景:
- 字段含
list或dict:内容相同但对象不同,==仍为True(深比较没问题),但若字段里存了自定义类且没实现__eq__,就直接比id,结果不可控 - 用
float字段:0.1 + 0.2 != 0.3,生成的__eq__会暴露这个坑 - 想让
name="Alice "和name="Alice"相等?必须手动重写__eq__,dataclass 不管这层语义
结论:dataclass 适合结构简单、字段全为不可变量的场景;一旦涉及业务规则、容错或特殊类型,老老实实手写 __eq__。
__eq__ 返回值必须是 bool,不是 True/False 对象
返回 1、0、np.bool_(True) 甚至自定义布尔子类,在某些上下文(比如 assert a == b、if a == b:)可能看似正常,但会破坏与内置函数的协作——all([a == b]) 或 pandas.Series.eq() 可能出错。
Python 明确要求 __eq__ 返回 bool 实例,否则属于未定义行为。
- 用
return bool(...)强制转,别依赖隐式转换 - 避免在
__eq__里调用可能返回非bool的第三方库方法(如旧版 NumPy 的比较结果) - 测试时加一句
assert isinstance(a == b, bool),尤其当你封装了复杂比较逻辑
最麻烦的不是写错,而是错得不报错——它安静地在某个边缘 case 里悄悄返回 numpy.bool_,然后在下游某个 json.dumps() 或数据库 ORM 映射时突然崩掉。










