有人问我为啥我先放结论呢,因为下面内容太多了,不想看又想找结论的同学们帮你们节约时间。
从python3.6开始,dict的插入变为有序,即字典整体变的有序;
而之前的版本,比如python2.7,对于字典的插入是乱序的,即插入a,b,c,返回结果顺序可能是a,c,b。
今天在做一个项目的时候,需要用到字典,但是又想数据按照字典key插入的顺序进行排序,根据python2.7的经验,字典是乱序的,于是在查资料的过程中发现了一个宝藏package叫OderedDict
,顾名思义就是有序字典,也就是按照插入顺序进行排序的一个字典。然后就用这个开发了呗。
但是一个偶然的机会有人问我能不能不用这个OderedDict
,因为还要import这个东西,太麻烦了。我寻思你嫌麻烦还是别写python了吧,这么简单的操作不适合你,但是还是要沉住气,憋住屁,帮他想想办法。然后我就去查python的dict,惊奇的发现python3的字典居然是有序的了?!
这令我十分诧异,于是便有了以下的源码探秘。
给一些新手们普及一些基础知识吧,这样在源码探秘过程中会容易理解一下。
在计算机科学中,关联数组(英语:Associative Array),又称映射(Map)、字典(Dictionary)是一个抽象的数据结构,它包含着类似于(键key,值value)的有序对。字典是一个可变容器,可以存储任意类型的对象。字典的实现和哈希算法也有紧密关系,key和value的映射便是通过哈希(hash)来实现的。
平均时间复杂度都是O(1)。注意这里提到的是平均时间复杂度,最坏情况是O(n),后文会提到原因。
问这个问题的大概没用过字典吧
通常来讲,不可以重复,如果重复赋值了,后面的值会覆盖前面的值。为啥说通常来讲呢,因为万一以后有什么神人搞了个什么奇怪的概念可以重复了呢(手动狗头)。
字典的底层实现实际就是一张hash表,简单可以理解为一个列表,列表中的每一个元素又保存了三个元素,分别是哈希值(hash value)、键(key)和值(value)。
entries = [
["--", "--", "--"],
["--", "--", "--"],
[hash_value_1, key1, value1],
[hash_value_2, key2, value2]
]
# 类似这个样子
通过这个列表,不难看出每两个元素之前存在一些空隙,这些空隙当然是给后续进来的值准备的啦,我们来看看是怎么实现插入的。
字典最小长度 - 1
,得到一个数字(index),这就是要插入的entries哈希表中的位置;我们通过代码来理解一下这个过程
my_dict = {
}
# 给字典添加一个值,key为hello,value为world
my_dict["hello"] = "world"
# 假设是一个空列表,hash表初始如下
entries = [
["--", "--", "--"],
["--", "--", "--"],
["--", "--", "--"],
["--", "--", "--"]
]
hash_value = hash("hello") # 假设值为 11111 注:以下计算值均为释例
index = hash_value & ( len(entries) - 1) # 假设index值计算后等于2
# 下面会将值存在entries中
entries = [
["--", "--", "--"],
["--", "--", "--"],
[11111, "hello", "world"], # index=2
["--", "--", "--"]
]
# 我们继续向字典中添加值
my_dict["benz"] = "car"
hash_value = hash("benz") # 假设值又为 11111
index = hash_value & ( len(entries) - 1) # 假设index值计算后同样等于2
# 下面会将值存在entries中
entries = [
["--", "--", "--"],
["--", "--", "--"],
[11111, "hello", "world"], # 由于index=2的位置已经被占用,且key不一样,所以判定为hash conflict,向下寻找空位
[11111, "benz", "car"] # 找到空余位置并进行插入
]
通过上面的原理解读和代码流程讲解,我们已经了解了字典的插入的过程。我们可以看到,不同的key通过hash计算的出的index值可能是不一样的,在entries中插入的位置不一样,所以当我们遍历字典的时候,字段的顺序与我们插入的顺序是会出现不相同的。
以前的字典只有一张简单的hash table,新的字典不仅有一张hash table,还有一张indices索引表加以辅助,于是便可以实现字典的有序。
新的结构:
indices = [index0, index0, index0, None]
entries = [
[hash_value_0, key0, value0],
[hash_value_1, key1, value1],
[hash_value_2, key2, value2],
["--", "--", "--"],
]
具体的实现过程又有怎样的变化呢?
字典最小长度 - 1
,得到一个数字(index),这就是要插入的indices索引表中的位置;下面从代码实现上我们再来看看
my_dict = {
}
# 给字典添加一个值,key为hello,value为world
my_dict["hello"] = "world"
# 假设是一个空列表,hash表初始如下
indices = [None, None, None, None]
entries = [
["--", "--", "--"],
["--", "--", "--"],
["--", "--", "--"],
["--", "--", "--"]
]
hash_value = hash('hello') # 假设值为 11111
index = hash_value & ( len(indices) - 1) # 假设index值计算后等于2
# 会找到indices的index为2的位置,并插入entries的长度
indices = [None, None, 0, None]
# 此时entries会插入这个元素
entries = [
["--", "--", "--"],
["--", "--", "--"],
[11111, "hello", "world"],
["--", "--", "--"]
]
# 我们继续向字典中添加值
my_dict["benz"] = "car"
hash_value = hash("benz") # 假设值为 22222
index = hash_value & ( len(indices) - 1) # 假设index值计算后等于 0
# 会找到indices的index为0的位置,并插入entries的长度
indices = [1, None, 0, None]
# 此时entries会修改相应index的值
entries = [
[22222, "benz", "car"],
["--", "--", "--"],
[11111, "hello", "world"],
["--", "--", "--"],
]
因此,在有indices表加持的情况下,整个hash table可以变的有序起来,这也就解释了为什么dict可以有序。
[Python-Dev] Python 3.6 dict becomes compact and gets a private version; and keywords become ordered. https://mail.python.org/pipermail/python-dev/2016-September/146327.html