字典是通过键(key)索引的,因此,字典也可视作彼此关联的两个数组。下面我们尝试向字典中添加3个键/值(key/value)对:

  1. >>> d = {'a': 1, 'b': 2}
  2. >>> d['c'] = 3
  3. >>> d
  4. {'a': 1, 'b': 2, 'c': 3}

这些值可通过如下方法访问:

  1. >>> d['a']
  2. 1
  3. >>> d['b']
  4. 2
  5. >>> d['c']
  6. 3
  7. >>> d['d']
  8. Traceback (most recent call last):
  9. File "<stdin>", line 1, in <module>
  10. KeyError: 'd'

由于不存在 'd' 这个键,所以引发了KeyError异常。

哈希表(Hash tables)

在Python中,字典是通过哈希表实现的。也就是说,字典是一个数组,而数组的索引是键经过哈希函数处理后得到的。哈希函数的目的是使键均匀地分布在数组中。由于不同的键可能具有相同的哈希值,即可能出现冲突,高级的哈希函数能够使冲突数目最小化。Python中并不包含这样高级的哈希函数,几个重要(用于处理字符串和整数)的哈希函数通常情况下均是常规的类型:

  1. >>> map(hash, (0, 1, 2, 3))
  2. [0, 1, 2, 3]
  3. >>> map(hash, ("namea", "nameb", "namec", "named"))
  4. [-1658398457, -1658398460, -1658398459, -1658398462]

在以下的篇幅中,我们仅考虑用字符串作为键的情况。在Python中,用于处理字符串的哈希函数是这样定义的:

  1. arguments: string object
  2. returns: hash
  3. function string_hash:
  4. if hash cached:
  5. return it
  6. set len to string's length
  7. initialize var p pointing to 1st char of string object
  8. set x to value pointed by p left shifted by 7 bits
  9. while len >= 0:
  10. set var x to (1000003 * x) xor value pointed by p
  11. increment pointer p
  12. set x to x xor length of string object
  13. cache x as the hash so we don't need to calculate it again
  14. return x as the hash

如果在Python中运行 hash('a') ,后台将执行 string_hash()函数,然后返回 12416037344 (这里我们假设采用的是64位的平台)。

如果用长度为 x 的数组存储键/值对,则我们需要用值为 x-1 的掩码计算槽(slot,存储键/值对的单元)在数组中的索引。这可使计算索引的过程变得非常迅速。字典结构调整长度的机制(以下会详细介绍)会使找到空槽的概率很高,也就意味着在多数情况下只需要进行简单的计算。假如字典中所用数组的长度是 8 ,那么键'a'的索引为:hash('a') & 7 = 0,同理'b'的索引为 3 ,'c'的索引为 2 , 而'z'的索引与'b'相同,也为 3 ,这就出现了冲突。

可以看出,Python的哈希函数在键彼此连续的时候表现得很理想,这主要是考虑到通常情况下处理的都是这类形式的数据。然而,一旦我们添加了键'z'就会出现冲突,因为这个键值并不毗邻其他键,且相距较远。

当然,我们也可以用索引为键的哈希值的链表来存储键/值对,但会增加查找元素的时间,时间复杂度也不再是 O(1) 了。下一节将介绍Python的字典解决冲突所采用的方法。

开放寻址法( Open addressing )

开放寻址法是一种用探测手段处理冲突的方法。在上述键'z'冲突的例子中,索引 3 在数组中已经被占用了,因而需要探寻一个当前未被使用的索引。增加和搜寻键/值对需要的时间均为 O(1)。

搜寻空闲槽用到了一个二次探测序列(quadratic probing sequence),其代码如下:

  1. j = (5*j) + 1 + perturb;
  2. perturb >>= PERTURB_SHIFT;
  3. use j % 2**i as the next table index;

循环地5*j+1可以快速放大不影响初始索引的哈希值二进位的微小差异。变量perturb可使其他二进位也不断变化。

出于好奇,我们来看一看当数组长度为 32 时的探测序列,j = 3 -> 11 -> 19 -> 29 -> 5 -> 6 -> 16 -> 31 -> 28 -> 13 -> 2…

关于探测序列的更多介绍可以参阅dictobject.c的源码。文件的开头包含了对探测机理的详细介绍。

下面我们结合例子来看一看 Python 内部代码。

基于C语言的字典结构

以下基于C语言的数据结构用于存储字典的键/值对(也称作 entry),存储内容有哈希值,键和值。PyObject 是 Python 对象的一个基类。

  1. typedef struct {
  2. Py_ssize_t me_hash;
  3. PyObject *me_key;
  4. PyObject *me_value
  5. } PyDictEntry;

下面为字典对应的数据结构。其中,ma_fill为活动槽以及哑槽(dummy slot)的总数。当一个活动槽中的键/值对被删除后,该槽则被标记为哑槽。ma_used为活动槽的总数。ma_mask值为数组的长度减 1 ,用于计算槽的索引。ma_table为数组本身,ma_smalltable为长度为 8 的初始数组。

  1. typedef struct _dictobject PyDictObject;
  2. struct _dictobject {
  3. PyObject_HEAD
  4. Py_ssize_t ma_fill;
  5. Py_ssize_t ma_used;
  6. Py_ssize_t ma_mask;
  7. PyDictEntry *ma_table;
  8. PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
  9. PyDictEntry ma_smalltable[PyDict_MINSIZE];
  10. };

字典初始化

字典在初次创建时将调用PyDict_New()函数。这里删掉了源代码中的部分行,并且将C语言代码转换成了伪代码以突出其中的几个关键概念。

  1. returns new dictionary object
  2. function PyDict_New:
  3. allocate new dictionary object
  4. clear dictionary's table
  5. set dictionary's number of used slots + dummy slots (ma_fill) to 0
  6. set dictionary's number of active slots (ma_used) to 0
  7. set dictionary's mask (ma_value) to dictionary size - 1 = 7
  8. set dictionary's lookup function to lookdict_string
  9. return allocated dictionary object

添加项

添加新的键/值对调用的是PyDict_SetItem()函数。函数将使用一个指针指向字典对象和键/值对。这一过程中,首先会检查键是否是字符串,然后计算哈希值,如果先前已经计算并缓存了键的哈希值,则直接使用缓存的值。接着调用insertdict()函数添加新键/值对。如果活动槽和空槽的总数超过数组长度的2/3,则需调整数组的长度。为什么是 2/3 ?这主要是为了保证探测序列能够以足够快的速度找到空闲槽。后面我们会介绍调整长度的函数。

  1. arguments: dictionary, key, value
  2. returns: 0 if OK or -1
  3. function PyDict_SetItem:
  4. if key's hash cached:
  5. use hash
  6. else:
  7. calculate hash
  8. call insertdict with dictionary object, key, hash and value
  9. if key/value pair added successfully and capacity over 2/3:
  10. call dictresize to resize dictionary's table

inserdict() 使用搜寻函数 lookdict_string() 来查找空闲槽。这跟查找键所用的是同一函数。lookdict_string() 使用哈希值和掩码计算槽的索引。如果用“索引 = 哈希值&掩码”的方法未找到键,则会用调用先前介绍的循环方法探测,直至找到一个空闲槽。第一轮探测,如果未找到匹配的键的且探测过程中遇到过哑槽,则返回一个哑槽。这可使优先选择先前删除的槽。

现在我们想添加如下的键/值对:{‘a’: 1, ‘b’: 2′, ‘z’: 26, ‘y’: 25, ‘c’: 5, ‘x’: 24},那么将会发生如下过程:

分配一个字典结构,内部表的尺寸为8。

以下就是我们目前所得到的:

8个槽中的6个已被使用,使用量已经超过了总容量的2/3,因而,dictresize()函数将会被调用,用以分配一个长度更大的数组,同时将旧表中的条目复制到新的表中。

在我们这个例子中,dictresize()函数被调用后,数组长度调整后的长度不小于活动槽数量的 4 倍,即minused = 24 = 4*ma_used。而当活动槽的数量非常大(大于50000)时,调整后长度应不小于活动槽数量的2倍,即2*ma_used。为什么是 4 倍?这主要是为了减少调用调整长度函数的次数,同时能显著提高稀疏度。

新表的长度应大于 24,计算长度值时会不断对当前长度值进行升位运算,直到大于 24,最终得到的长度是 32,例如当前长度为 8 ,则计算过程如8 -> 16 -> 32。

这就是长度调整的过程:分配一个长度为 32 的新表,然后用新的掩码,也就是 31 ,将旧表中的条目插入到新表。最终得到的结果如下:

删除项

删除条目时将调用PyDict_DelItem()函数。删除时,首先计算键的哈希值,然后调用搜询函数返回到该条目,最后该槽被标记为哑槽。

假设我们想要从字典中删除键'c',我们最终将得到如下结果:

注意,删除项目后,即使最终活动槽的数量远小于总的数量也不会触发调整数组长度的动作。但是,若删减后又增加键/值对时,由于调整长度的条件判断基于的是活动槽与哑槽的总数量,因而可能会缩减数组长度。

作者:佚名

来源:51CTO

深入Python字典的内部实现相关推荐

  1. Python字典类型内部做判断赋值

    # coding=utf-8import sysaaa = 1 bbb = 2ccc = {"zhaoyun":"abc" if aaa == 2else &q ...

  2. python字典get计数_Python内部是如何存储GC引用变量的计数的?

    这段时间一直在想一个问题,为什么Python有了GIL依然还要对变量加锁.Google的过程中查看一些东西,有了新的困惑. 一个说法说Python内部保存了一个用户空间和一个内核空间.用户空间通常就是 ...

  3. html读取字典endfor,Flask和HTML,从python字典迭代到HTML表

    我有一本python字典data = { 'category1': { 'titles': ['t1', 't2', 't3'], 'dates': ['d1', 'd2', 'd3'] }, 'ca ...

  4. python中的字典推导式_17.python 字典推导式(经典代码)

    在昨天的文章中,我们介绍了关于** python列表推导式** 的使用,字典推导式使用方法其实也类似,也是通过循环和条件判断表达式配合使用,不同的是字典推导式返回值是一个字典,所以整个表达式需要写在{ ...

  5. python代数式的表达方式_关于python字典类型最疯狂的表达方式

    一个Python字典表达式谜题 让我们探究一下下面这个晦涩的python字典表达式,以找出在python解释器的中未知的内部到底发生了什么. # 一个python谜题:这是一个秘密 # 这个表达式计算 ...

  6. Python 字典推导式 - Python零基础入门教程

    目录 一.Python 字典推导式简介 二.Python 字典推导式语法 三.Python 字典推导式实战 1.在字典中提取或者修改数据,返回新的字典 2.在字符串中提取数据,返回新的字典 四.重点总 ...

  7. python字典中的值只能是字符串类型_python字典key不能是可以是啥类型

    python中字典的key不能是可变类型.字典可存储任意类型对象,其中值可以取任何数据类型,但键必须是不可变的,如字符串.数字或元组.语法格式:[d = {key1 : value1, key2 : ...

  8. python字典去重合并_Python字典及基本操作(超级详细)

    Python字典及基本操作(超级详细) 收录于话题#Python入门27个 今天小张帮大家简单介绍下Python的一种数据结构: 字典,字典是 Python 提供的一种常用的数据结构,它用于存放具有映 ...

  9. python 字典性质描述_卧槽!Python还有这些特性(2):奇怪的字典

    (给Python开发者加星标,提升Python技能)英文:Satwik Kansal,翻译:暮晨 Python开发者整理自 GitHub [导读]:Python 是一个设计优美的解释型高级语言,它提供 ...

最新文章

  1. Springboot 利用AOP编程实现切面日志
  2. 如何在JavaScript中实现链接列表
  3. 那些实用与颜值齐飞的桌面!
  4. Leetcode - 347. Top K Frequent Elements(堆排序)
  5. 检查SELECTION-SCREEN 上面输入的参数是否合适
  6. Matlab | Matlab从入门到放弃(3)——函数与画图
  7. Centos 7 查看内存占用情况相关命令
  8. 关于 Oracle分页数据重复的问题
  9. oledb 操作 excel
  10. docker 定时重启脚本_使用 Go 添加启动脚本
  11. 【Linux】linux下查看目录所在分区
  12. Nginx Parsing HTTP Package、header/post/files/args Sourcecode Analysis
  13. SSM项目使用Idea打war包
  14. Java POJO Bean 对象与 Web Form 表单的自动装配
  15. python小人画爱心_使用Python画出小人发射爱心的代码
  16. 增广拉格朗日 matlab,大连理工优化方法增广拉格朗日方法MATLAB程序
  17. 2020 中国大学生计算机设计大赛
  18. Deep Adversarial Decomposition: A Unified Framework for Separating Superimposed Images 论文阅读笔记
  19. oracle oem登录xdb,XDB sys_nc_oid$递归调用的案例一则
  20. IcedTea:首个100%兼容、开源的Java

热门文章

  1. php 检查路劲是否存在,php 检查文件或目录是否存在代码总结
  2. 在计算机组成原理中x,计算机组成原理xu2.ppt
  3. php-fpm 的参数,php-fpm启动参数配置详解
  4. HarmonyOS分布式软总线研究,【钟洪发老师公开课】实战学习HarmonyOS重点之分布式软总线...
  5. 不使用输入框如何实现下拉_如何利用Axure实现下拉子菜单?
  6. html ie8上传图片,图片上传本地预览兼容ie8
  7. 阅读吴军《信息时代对人才技能的要求》
  8. 《天天数学》连载07:一月七日
  9. 【codevs1477】【BZOJ2733】永无乡,Splay+启发式合并
  10. 7 centos lvs nat配置_centos中lvs安装配置方法详解