重写 __eq__ 后对象默认不可哈希,因 Python 自动将 __hash__ 设为 None;若需可哈希,必须显式定义满足“等值对象哈希一致”的 __hash__,且仅基于 __eq__ 中使用的不可变字段计算。

重写 __eq__ 后对象变不可哈希,会直接报 TypeError: unhashable type
Python 默认所有自定义类实例都是可哈希的(继承自 object.__hash__),但一旦你显式重写了 __eq__,Python 就自动把 __hash__ 设为 None。这不是警告,是硬性禁用:只要 __hash__ 是 None,该类型就无法作为 dict 的键、set 的元素或 frozenset 的成员。
常见触发场景:
-
cache[my_obj] = value—— 报错 -
seen = {obj1, obj2}—— 报错 -
dict.fromkeys([obj1, obj2])—— 报错
这不是“建议”,是 Python 解释器在运行时强制执行的契约:可哈希对象必须满足「相等的对象有相同的哈希值」,而重写 __eq__ 意味着你改变了“相等”的定义,原有默认哈希(基于 id())就不再匹配了。
为什么不能只靠 __eq__,非得加 __hash__?
哈希结构(如 set、dict)不是靠暴力遍历 + __eq__ 去重的。它先调用 __hash__ 定位桶(bucket),再对同桶内少数几个对象调用 __eq__ 精确比对。没有一致的 __hash__,两个逻辑上相等的对象可能被分到不同桶里,__eq__ 根本没机会被调用。
立即学习“Python免费学习笔记(深入)”;
例如:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return isinstance(other, Point) and self.x == other.x and self.y == other.y
<p>p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # True
print({p1, p2}) # TypeError: unhashable type: 'Point'
即使你手动补上 __hash__,也必须确保:若 p1 == p2,则 hash(p1) == hash(p2);否则 set 仍可能存入重复项,或 dict 查不到已存在的键。
怎么写一个安全的 __hash__?
核心原则:只对参与 __eq__ 判断的**不可变字段**计算哈希。如果字段可变(比如含 list 或 dict),要么拒绝哈希(返回 NotImplemented 或抛异常),要么确保这些字段在对象生命周期内不被修改(否则哈希值失效,容器行为未定义)。
实操建议:
- 用
hash((self.x, self.y))而不是hash(self.x) ^ hash(self.y)(后者易碰撞) - 如果
__eq__比较的是全部属性,且它们都不可变,就用hash((attr1, attr2, ...)) - 如果类支持部分字段参与比较(比如忽略时间戳),
__hash__必须和这些字段完全一致 - 不要在
__hash__中调用可变对象的__hash__(如hash(self.data_list)),列表本身不可哈希
修正上面的 Point:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return isinstance(other, Point) and self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y)) # ✅ 安全:x、y 不变,且参与 __eq__
哪些情况其实可以不写 __hash__?
如果你的类明确**永远不会用作哈希容器的键或元素**,比如纯数据传输对象(DTO)、临时计算中间体、或仅用于日志/序列化的对象,那就不必强求实现 __hash__。Python 不会因为你没写就报错——只在你真把它塞进 set 或 dict 时才拦。
但要注意:一旦有人(包括你自己未来)尝试 set([obj]) 或 {obj: 1},错误就会立刻出现。所以更稳妥的做法是——要么不重写 __eq__,要么连带把 __hash__ 补全。省掉一次 __hash__,可能换来三次调试时间。


















