Python中字典的几个问题

本文探讨了Python字典的工作原理,强调了键的唯一性和哈希函数的重要性。字典通过键的哈希值快速定位值,强调了不可变对象作为键的原因,因为可变对象的哈希值会随内容变化,导致查询失败。同时,文章解释了字典的存储逻辑和Python处理不存在键的机制,包括getitem、missing方法和defaultdict类。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

字典由包含键(key)和值(value)的项组成。

# 创建空字典
dic1 = dict()
dic2 = {}  
# 从可迭代对象中创建字典
h = [(str(i), i) for i in range(10)]
dic1 = dict(h)
dic2 = {key: value for key, value in h}  #也叫做字典推导

以上两种方式都可以创建空的字典。基于python对可迭代对象的偏爱,从可迭代对象创建字典也不足为奇。

字典对值的要求可以用一句话做概括,爱咋的咋的,因为字典不会对值做检查,只是建立起一个键值对应关系。
主要的讲究在于键,字典是时间高效性,就是基于键的唯一性(单射)。简单来说,我们可以通过键查询到对应的值,然后返回它,而不用进行逐个扫描。

来分析下这是怎么做到的。
假设我们存了一个数组,包含1到10。有两种存法

  • 建立一个10元素的数组,顺序不管,塞进去就完事
  • 建立一个10元素的数组,每个元素按顺序存入,第0个是1,第1个是2

现在把这个数组交给别人,问他10在不在里面。
如果是第一种存法,他需要逐个比较每个元素,看看10在不在数组里。第二种存法,直接可以知道10在第9个位置(从0算起)。所以如果能直接得到位置,这样查询操作的复杂度就是O(1)。

扩展下这种思维, 给你一个对象,根据对象算出一个位置,你就把他存在这个位置。这就是Python中集合(set)的做法。
字典跟集合的不同之处在于,字典根据键来算位置,而集合直接用元素来算位置。
现在就可以考虑这个位置怎么算的问题了。我们把算位置的方法叫做哈希函数,给定key,使用hash算得到地址。什么是好的hash,可以参考算法书籍。

我们这里来讨论什么样的对象是可以作为key。
先从应用的角度分析下,什么样的key适合
首先我们比较两个东西,肯定是根据人的主观来比较,比如
a=1, b=1,我们认为值相等就是相同的,而不会依据他们存储的位置是否相同来判断他们是否相等。
所以我们希望所见即所得的方式。这也是为什么eq和hash必须等价。可以通俗的认为, hash是机器看的,eq是人看的。机器的实现逻辑必须按照人的感觉来。

为什么可变对象不可hash:
假设我们硬要对可变对象进行hash。有什么办法可以用

第一,我们对内存地址(id)hash: 用id做为hash不是一个好办法,为啥,如果两个对象的值相等,比如a=[1,2], b=[1,2]。显然我们认为他们是相等的,但是内存地址是显然不相等的,所以两者的hash不相等,这是机器违背了人的直觉,不要。
第二,我们对内容hash: 对内容hash是可以的,而且tuple就是这么做的,似乎没毛病。但是tuple是不可变的,对可变这么做是有问题的,这就牵扯到key的查询过程。

上述的查询过程似乎是一步过程,键算hash,取出对应hash的值。但实际上这种操作没办法检查hash冲突的状况,所以实际上查询过程是两步过程。如下

第一:对查询的键值用哈希函数计算得到一个位置,取出该位置的key。
第二:对比得到的key是否与查询的key相同。
如果相同返回该key对应value,不同就使用解决冲突的办法继续重复上述两步。直到key相等,或者得到的地址为空。
所以为了解决冲突,比较key是否相同是必需的操作

在回头看下用可变对象的内容作为键值会发生什么,
首先建立一个可变对象a和可变对象b, 他们具有相同的内容,将a作为键,存入一个字典元素,此时hash是根据可变对象的内容来算的。
这时候不做改动,a,b作为键都是可以取出对应的值。
现在改变a的内容,此时用a作为键来查询值,hash变化了,因为内容变化了,所以查询失败。那么用b作为键查询会发生什么,b的hash没有变化,对应位置有值,取出该键,接下来比较键是否相等,由于a的内容变化,键存储的是a,此时a不等b,所以查询失败。唯一能查询成功的就是a回退到初始状态,也就是不要改动a,这就相当于不可变对象作为键。

这里还有个问题, 为什么键值会变,这是需要了解字典的存储逻辑。

接下来从函数实现的方面看看Python字典的逻辑。

从实现角度来说并没有实现hash办法的对象是不可hash的,这就意味着不能作为键。

dic[key]的背后逻辑和运行规律是啥
首先,我们使用getitem查询是否存在,
如果查询失败,调用missing处理,
这时候missing会使用default_factory等办法,努力解决不存在键的问题
如果missing也不能为力,就报错。
所以python的备胎机制也是很完善的。

注意备胎missing是专一的, 也就是说他只对getitem的异常进行处理,其他的get,contain方法是不会调用的。这是符合逻辑的,contain的办法本来就是查询存在性的,get方法自带不存在时的反应策略,所以missing办法是不被期待的,但是好像又有点逻辑不一致?????
contain, get, setdefault,

如果我们知道可能不存在键,并且对不存在的键有些想法,比如加入字典。这时候可以用三种策略,
第一:get(key, default), get可以设置缺省返回值,但是这个返回值不会被放入字典,所以加入字典需要额外操作
第二:setdefault(key, default),这时候也有返回值,与get的区别是返回值默认被加入字典。这个办法其实已经足够有效,缺点就是代码比较丑
第三:创建defaultdict,这个类在collections当中。该字典会默认处理不存在情况,等价于第二种方法,但是不需要显示调用,直接用dic[key]就可以了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值