字典由包含键(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]就可以了