必须重写__hash__,因为重写__eq__后Python自动设__hash__为None以保证哈希一致性;安全做法是仅基于不可变且参与__eq__判断的属性用hash((a,b,c))计算。

为什么定义了 __eq__ 还要重写 __hash__?
Python 默认让自定义类实例不可哈希(TypeError: unhashable type),哪怕你只改了 __eq__。因为 Python 要求:如果两个对象 __eq__ 返回 True,它们的 __hash__ 必须相等;反之,如果 __hash__ 不同,__eq__ 就不该返回 True。一旦你重写了 __eq__,Python 就自动把 __hash__ 设为 None,强制你显式定义它,避免违反哈希一致性。
怎么写一个安全的 __hash__?
核心原则:只基于那些参与 __eq__ 判断、且**不可变**的属性来计算哈希值。否则对象放进 set 或作为 dict 键后,属性一改,哈希值就变,导致查不到、重复插入、甚至破坏哈希表结构。
- 用
hash((a, b, c))对元组求哈希——最常用、最稳妥,前提是a、b、c本身可哈希且不变 - 不要用
id(self)或随机数——这会让相等对象哈希不同,违背约定 - 不要在
__hash__里访问可变属性(比如列表、字典),哪怕它们参与了__eq__——运行时可能报TypeError或逻辑错乱 - 如果类设计上允许修改关键字段,就别让它可哈希;真要支持,得靠
__slots__+ 属性冻结,或手动抛TypeError在 setter 里
__eq__ 和 __hash__ 的典型配对写法
以一个表示二维点的类为例:
class Point:
__slots__ = ('x', 'y') # 防止动态加属性,强化不可变语义
<pre class="brush:php;toolbar:false;">def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return False
return self.x == other.x and self.y == other.y
def __hash__(self):
return hash((self.x, self.y))
立即学习“Python免费学习笔记(深入)”;
注意三点:
librosa 是一个用于音频和音乐分析的 Python 库。它提供了特征提取、频谱分析、节拍跟踪、起始检测和音频可视化的工具,基于 NumPy 和 SciPy 构建,用于科学音频计算。
-
__slots__不是必须的,但它能阻止意外赋值,配合文档说明“此对象应视为不可变”,减少误用 -
__eq__里做了类型检查,避免和非Point对象比较时出错(比如Point(1,2) == [1,2]返回False而不是抛异常) - 如果
x或y是浮点数,要考虑精度问题:0.1 + 0.2 != 0.3,这时候__eq__可能需要math.isclose,但__hash__仍得用原始值——否则哈希不一致。这种场景下,干脆别让它可哈希
常见错误:可变对象强行哈希
下面这段代码看着能跑,但极其危险:
class BadContainer:
def __init__(self, items):
self.items = items # list,可变
<pre class="brush:php;toolbar:false;">def __eq__(self, other):
return self.items == other.items
def __hash__(self):
return hash(tuple(self.items)) # ❌ 危险!items 可能被外部修改
立即学习“Python免费学习笔记(深入)”;
一旦你把它放进 set,再修改 items,这个对象在集合里的位置就“找不到了”:
-
s = {BadContainer([1,2])}→ 插入成功 -
s.pop().items.append(3)→ 对象还在s里,但哈希值已变 -
BadContainer([1,2,3]) in s→False,即使它“逻辑上”等于原对象 - 更糟的是,
s内部状态可能损坏,后续操作崩溃
真正安全的做法,要么用 tuple 替代 list 当属性,要么放弃可哈希性——这点容易被忽略,尤其在快速原型阶段。

















