自定义类实例默认不能当字典键,因为其__hash__基于对象身份且与__eq__绑定;若重写__eq__需同时实现__hash__,且参与哈希的属性必须不可变,否则破坏哈希契约。

为什么自定义类实例默认不能当字典键?
因为 Python 的 dict 要求键必须是可哈希的(hashable),而默认情况下,自定义类实例的 __hash__ 返回的是基于对象身份(id())的值,且同时会把 __eq__ 设为基于身份比较。一旦你重写了 __eq__(比如按属性值判断相等),Python 就会自动把 __hash__ 设为 None,导致报错:TypeError: unhashable type。
必须同时实现 __eq__ 和 __hash__
只要你想让实例按“逻辑值”相等(而非内存地址),就必须成对重写这两个方法。否则要么无法放入字典,要么出现违反哈希契约的行为(相同值的实例哈希不同,或不同值却哈希相同)。
-
__eq__决定两个实例是否“相等”,通常基于某些不可变属性(如self.name,self.id) -
__hash__必须返回一个整数,且**相等的对象必须有相同的哈希值**;推荐直接对参与__eq__判断的元组调用hash() - 参与哈希计算的属性必须是不可变的——如果它们在实例生命周期中被修改,哈希值就失效,字典查找会出错
示例:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
<pre class="brush:php;toolbar:false;">def __eq__(self, other):
if not isinstance(other, Person):
return False
return self.name == other.name and self.age == other.age
def __hash__(self):
return hash((self.name, self.age)) # 元组可哈希,且顺序固定
立即学习“Python免费学习笔记(深入)”;
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
这样 Person("Alice", 30) 就能作为字典键正常使用了。
常见踩坑点:可变属性、__slots__ 和继承影响
最容易忽略的是:即使你写了 __hash__,如果实例属性被后续修改,哈希值就和原始计算结果不一致,导致字典“找不到”本该存在的键。
- 避免在
__hash__中使用可变对象(如list、dict、自定义可变类实例)——它们本身不可哈希,且内容变化后无法保证一致性 - 如果用了
__slots__,确保所有参与__hash__和__eq__的字段都在__slots__里声明,否则可能因动态属性引发意外行为 - 子类继承父类的
__hash__但重写__eq__时没同步更新__hash__,会导致哈希与相等逻辑脱节
什么时候不该实现 __hash__?
如果你的类天然需要支持状态变更(比如带缓存、计数器、IO句柄),或者核心属性本身就是可变的(如 self.data = []),那就不该让它可哈希。强行加 __hash__ 只会让使用者掉进陷阱——字典键“消失”、集合去重失效、甚至调试时完全想不通为什么 in 检查返回 False。
真正关键的不是“能不能写 __hash__”,而是“这个对象的‘值’是否稳定、是否适合做唯一标识”。多数业务模型类其实更适合用 dataclass(frozen=True) 或 NamedTuple,从设计上堵死可变性漏洞。

















