什么?python dict字典有序了?!

结论 太长不看系列

有人问我为啥我先放结论呢,因为下面内容太多了,不想看又想找结论的同学们帮你们节约时间。

从python3.6开始,dict的插入变为有序,即字典整体变的有序;
而之前的版本,比如python2.7,对于字典的插入是乱序的,即插入a,b,c,返回结果顺序可能是a,c,b。

文章目录

  • 结论 ~~太长不看系列~~
  • 背景
  • 基础知识
    • 什么是字典?
    • 字典的查询、添加、删除的时间复杂度?
    • 字典的key可以重复么?
  • 源码探秘
    • python3.6之前
    • python3.6之后
  • 参考资料

背景

今天在做一个项目的时候,需要用到字典,但是又想数据按照字典key插入的顺序进行排序,根据python2.7的经验,字典是乱序的,于是在查资料的过程中发现了一个宝藏package叫OderedDict,顾名思义就是有序字典,也就是按照插入顺序进行排序的一个字典。然后就用这个开发了呗。

但是一个偶然的机会有人问我能不能不用这个OderedDict,因为还要import这个东西,太麻烦了。我寻思你嫌麻烦还是别写python了吧,这么简单的操作不适合你,但是还是要沉住气,憋住屁,帮他想想办法。然后我就去查python的dict,惊奇的发现python3的字典居然是有序的了?!

这令我十分诧异,于是便有了以下的源码探秘。

基础知识

给一些新手们普及一些基础知识吧,这样在源码探秘过程中会容易理解一下。

什么是字典?

在计算机科学中,关联数组(英语:Associative Array),又称映射(Map)、字典(Dictionary)是一个抽象的数据结构,它包含着类似于(键key,值value)的有序对。字典是一个可变容器,可以存储任意类型的对象。字典的实现和哈希算法也有紧密关系,key和value的映射便是通过哈希(hash)来实现的。

字典的查询、添加、删除的时间复杂度?

平均时间复杂度都是O(1)。注意这里提到的是平均时间复杂度,最坏情况是O(n),后文会提到原因。

字典的key可以重复么?

问这个问题的大概没用过字典吧
通常来讲,不可以重复,如果重复赋值了,后面的值会覆盖前面的值。为啥说通常来讲呢,因为万一以后有什么神人搞了个什么奇怪的概念可以重复了呢(手动狗头)。

源码探秘

python3.6之前

字典的底层实现实际就是一张hash表,简单可以理解为一个列表,列表中的每一个元素又保存了三个元素,分别是哈希值(hash value)、键(key)和值(value)。

entries = [
	["--", "--", "--"],
	["--", "--", "--"],
	[hash_value_1, key1, value1],
	[hash_value_2, key2, value2]
]
# 类似这个样子

通过这个列表,不难看出每两个元素之前存在一些空隙,这些空隙当然是给后续进来的值准备的啦,我们来看看是怎么实现插入的。

  1. 通过hash函数对key进行hash,得到hash value;
  2. mask做与(and)操作,这里设定mask为字典最小长度 - 1,得到一个数字(index),这就是要插入的entries哈希表中的位置;
  3. 若index下标位置已经被占用,则会判断entries的key是否与要插入的key是否相等;
  4. 如果key相等就表示key已存在,则更新value值;
  5. 如果key不相等,就表示hash conflict,出现哈希冲突,则会继续向下寻找空位置,一直到找到剩余空位为止。因此,若所有位置都被占满,则需要O(n)的时间复杂度来遍历整个列表。

我们通过代码来理解一下这个过程

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中插入的位置不一样,所以当我们遍历字典的时候,字段的顺序与我们插入的顺序是会出现不相同的。

python3.6之后

以前的字典只有一张简单的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. 通过hash函数对key进行hash,得到hash value;
  2. mask做与(and)操作,这里设定mask为字典最小长度 - 1,得到一个数字(index),这就是要插入的indices索引表中的位置;
  3. 得到index后,对应到indices的位置,但是此位置不是存的hash value,而是存序号,表示该值在entries中的位置;
  4. 若无冲突,则插入entries中的相应元素;
  5. 冲突处理与之前一致。

下面从代码实现上我们再来看看

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

你可能感兴趣的:(问题,python,算法,数据结构)