尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python 的哈希契约:为什么 list 不能当字典的键
原文链接Python 的哈希契约为什么 list 不能当字典的键 - 码日新 · 每日文章Python 的哈希契约为什么 list 不能当字典的键上周帮同事看一个缓存问题他用列表拼了个 key运行时直接抛出TypeError: unhashable type: list。改成元组就好了。但另一个更隐蔽的 bug 是对象明明还在字典里却怎么也取不出来。这两个问题其实指向同一条规则——能当字典键的东西必须遵守哈希契约。一次d[key]到底发生了什么Python 的dict本质是哈希表。执行d[key]时解释器大致做两件事计算hash(key)用结果定位到某个桶在桶里用比较候选键。为了效率比较之前还会先看是不是同一个对象身份相同就直接命中。classKey:def__init__(self,value):self.valuevaluedef__hash__(self):print(fhash({self.value}))returnhash(self.value)def__eq__(self,other):print(feq({self.value},{getattr(other,value,other)}))returnisinstance(other,Key)andself.valueother.value d{Key(1):a}print(--- 查找 ---)print(d[Key(1)])输出hash(1) --- 查找 --- hash(1) eq(1, 1) a这也解释了字典的两条硬性要求相等的键必须有相同的哈希键的哈希在它待在字典里的时候不能变。违反任何一条字典就可能找不到东西。list 不行tuple 也不一定行list是可变的所以 Python 直接把它的__hash__设成了Noneprint(list.__hash__)# Noneprint(hash((1,2)))# 正常print(hash((1,[2])))# TypeError: unhashable type: list元组本身不可变但它的哈希是由元素组合出来的。只要里面有一个元素不可哈希整个元组就不可哈希。这个规则会递归传递hash((1,(2,3)))# 可以hash((1,(2,[3])))# TypeError所以“把 list 改成 tuple”并不是万能药。如果列表里还藏着列表或字典照样会失败。写了__eq____hash__就没了自定义类默认按身份哈希也就是基于id()。两个内容相同的对象默认就是两个不同的键。一旦你实现了__eq__Python 会把__hash__置为None除非你也显式实现它classUser:def__init__(self,uid,name):self.uiduid self.namenamedef__eq__(self,other):returnisinstance(other,User)andself.uidother.uidprint(User.__hash__)# None# hash(User(1, a)) # TypeError: unhashable type: User补上__hash__时字段必须和__eq__保持一致classUser:def__init__(self,uid,name):self.uiduid self.namenamedef__eq__(self,other):returnisinstance(other,User)andself.uidother.uiddef__hash__(self):returnhash(self.uid)这里name可变但不参与相等判断也不参与哈希所以修改它不会破坏字典。如果整个对象都该不可变可以用冻结的 dataclassfromdataclassesimportdataclassdataclass(frozenTrue)classUser:uid:intname:str默认的dataclass(frozenTrue)会生成__eq__和__hash__并阻止属性赋值。注意它的哈希会用到所有参与比较的字段如果只想按uid判等还是得手写。最隐蔽的坑键被修改了下面这段代码不会报错但会让键“消失”classMutableKey:def__init__(self,value):self.valuevaluedef__hash__(self):returnhash(self.value)def__eq__(self,other):returnisinstance(other,MutableKey)andself.valueother.value kMutableKey(1)d{k:one}k.value2print(kind)# Falseprint(d.get(k))# Noneprint(list(d))# 键还在字典里print(d[k])# KeyError字典并没有坏。它只是按照新的哈希去 2 号桶找而当初存进去的是 1 号桶。旧条目还在但已经无法通过这个键访问。解决办法不是“改完再 rehash”而是别让键变用不可变对象当键只把不会变的字段放进__hash__真需要可变键就先删除旧键改完再插回去。相等但哈希不同也会出事还有一种更隐蔽的写法__eq__按值比较__hash__却按身份生成。classBadKey:def__init__(self,value):self.valuevaluedef__eq__(self,other):returnisinstance(other,BadKey)andself.valueother.valuedef__hash__(self):returnid(self)a,bBadKey(1),BadKey(1)print(ab)# Trueprint(hash(a)hash(b))# Falsed{a:a}print(d[b])# KeyErrorprint(len({a,b}))# 2两个对象相等却被放进了不同的桶。字典既无法保证“一个键只出现一次”也无法通过等价的键找到值。顺带一提1、1.0和Trued{1:int,1.0:float,True:bool}print(d)# {1: bool}三者相等哈希也相同所以它们是同一个键后写入的覆盖先写入的。这不是 bug而是哈希契约的正常结果。NaN 则是另一个极端nanfloat(nan)d{nan:x}print(d[nan])# x身份相同直接命中print(d[float(nan)])# KeyError新对象既不等于自己哈希也不同所以别拿 NaN 当键。如果数据里可能出现 NaN先清洗再入字典。实践清单想用的键能否直接用建议int/str/bytes是不可变哈希稳定tuple视元素而定元素必须全部可哈希list/dict/set否可变__hash__为Nonefrozenset是元素必须可哈希普通自定义对象是默认按id()身份语义实现了__eq__但没实现__hash__否__hash__会被置为None几条可以直接照做的建议组合键优先用元组嵌套的 list 转成 tupledict 转成tuple(sorted(d.items()))set 转成frozenset。自定义值对象要同时实现__eq__和__hash__并保证两者使用同一组字段。对象放进字典后不要修改参与哈希的字段。需要身份语义时就用默认的object.__hash__和object.__eq__不要多写。调试时可以直接打印Cls.__hash__或者在修改前后对比hash(k)。回到开头那个缓存把 list 换成 tuple 只是第一步真正要确认的是这个 key 在缓存生命周期里不会变。字典不会帮你检查这件事它只负责按照你给的哈希去找。哈希契约是调用方和维护者之间的约定Python 把签字笔递给了你。参考文档Primers 编程伙伴/Python/内建函数/typePrimers 编程伙伴/Python/内建函数/printPrimers 编程伙伴/Python/内建函数/hashPrimers 编程伙伴/Python/内建函数/getattrPrimers 编程伙伴/Python/内建函数/isinstancePrimers 编程伙伴/Python/内建函数/listPrimers 编程伙伴/Python/内建函数/intPrimers 编程伙伴/Python/内建函数/strPrimers 编程伙伴/Python/内建函数/idPrimers 编程伙伴/Python/内建函数/lenPrimers 编程伙伴/Python/内建函数/floatPrimers 编程伙伴/Python/内建函数/bool
RELATED

相关推荐

最小操作次数使数组元素相等 III【力扣LeetCode  每日一题】

最小操作次数使数组元素相等 III【力扣LeetCode 每日一题】

3736. 最小操作次数使数组元素相等 III 网页链接 力扣LeetCode 力扣LeetCode & 每日一题,完成每日打卡,即可获得力扣积分。获得相应数量的积分,能在【力扣商店】,换取相应奖品!助力每日有题做,丰盈积…

📅 2026/11/5 2:00:59
asyncio.run() cannot be called from a running event loop:在 async 函数里该怎么改

asyncio.run() cannot be called from a running event loop:在 async 函数里该怎么改

把一个本来能在命令行运行的异步示例搬进 Web 框架的 async 处理函数或支持顶层 await 的交互环境时,常见报错是: RuntimeError: asyncio.run() cannot be called from a running event loop这不是“异步函数不能调用异步函数”,而是当前线程…

📅 2026/11/5 2:00:59
Opus 4.8快速模式11月30日移除:两条迁移路径怎么选?

Opus 4.8快速模式11月30日移除:两条迁移路径怎么选?

如果 Claude Code 仍在使用 Opus 4.8,并且打开了快速模式,需要在 11 月 30 日前选择一条路径:继续用快速模式,就切换到支持它的模型;继续用 Opus 4.8,就关闭快速模式。 官方文档明确:Opus 4.8 快…

📅 2026/11/5 2:00:59
MORE NEWS

更多资讯

📰

Java Web批量下载打包ZIP:流安全与中文文件名实战方案

简介:本资源是一套基于Java实现的多URL文件批量下载与ZIP打包导出的完整工程实践方案,面向Java初中级开发者及Web后端学习者,解决网络文件聚合分发场景下的核心需求:从多个远程URL并发下载资源,并自动整理、压缩为可直…

📰

数据湖存储架构设计与优化实战指南

1. 数据湖存储架构的本质解析数据湖作为大数据领域的核心基础设施,其存储设计直接决定了数据价值挖掘的效率和成本。与传统数据仓库相比,数据湖采用"先存储后定义"的模式,允许原始数据以原生格式保存,这种设计带来了极大…

📰

具身智能操作系统:架构设计与工程实践

1. 具身智能操作系统概述具身智能操作系统(Embodied AI OS)是近年来人工智能与机器人技术交叉融合的前沿领域。不同于传统操作系统仅管理计算资源,这类系统需要协调感知、决策、执行三大核心模块,实现智能体在物理环境中的自主交互…

📰

自动驾驶多传感器时空对齐技术实践

1. 项目背景与核心挑战在自动驾驶决策系统的开发过程中,多传感器数据融合的准确性直接关系到车辆对周围环境的理解能力。Apollo作为行业领先的自动驾驶开源平台,其决策算法需要处理来自激光雷达和视觉传感器的异构数据。这两种传感器在数据形式、采样频率…

📰

HarmonyOS智能座舱开发:分布式架构与实时性优化

1. 智能座舱行业现状与技术演进汽车行业正在经历百年未有的智能化变革,智能座舱作为人车交互的核心载体,其技术复杂度呈指数级增长。根据我参与过的三个整车厂项目数据,2023年主流车型的座舱代码量已突破5000万行,是传统车载信息娱…

📰

对话式诊断系统AMIE:从主诉到鉴别诊断的对话机制与评测体系

医学问诊这件事,长期以来有个尴尬的现实:患者描述症状是模糊的、跳跃的、带情绪的,而医生的诊断推理是结构化的、收敛的、讲证据的。这两者之间的鸿沟,靠一个医学院读八年的人去填,成本极高。大模型进来之后&#xff0…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬