字典内部剖析

开篇先提出几个疑问:

  • 所有的类型都可以做字典的键值吗?
  • 字典的存储结构是如何实现的?
  • 散列冲突时如何解决?

最近看了一些关于字典的文章,决定通过自己的理解把他们写下来;本章将详细阐述上面的几个问题,通过源码的剖析,尽量还原字典的真相。

键值要求:

在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开发【数据结构】:字典内部剖析的更多相关文章

  1. Python开发爆破字典

    这里只是分享一下Python如何生成爆破字典 关于爆破工具编写我会在下一篇提到 其实有了爆破字典的话,直接使用Burp,Hydra等一些工具就可以做爆破了! Burp的使用在我以前的博客中有写过,至于 ...

  2. (python)数据结构---字典

    一.描述 由键值key-value组成的数据的集合 可变.无序的,key不可以重复 字典的键key要可hash(列表.字典.集合不可哈希),不可变的数据结构是可哈希的(字符串.元组.对象.bytes) ...

  3. Python开发【源码剖析】 List对象

    前言 本文探讨的Python版本为2.7.16,可从官网上下载,把压缩包Python-2.7.16.tgz解压到本地即可 需要基本C语言的知识(要看的懂) PyListObject对象 PyListO ...

  4. Python常用数据结构-字典——2.1 字典方法 keys()

    python字典常用方法: keys()               #  获取所有的键 values()            #  获取所有的值 items()              #  获 ...

  5. python开发笔记-字典按值排序取前n个key值

    场景举例: 假如我们有某个班级的语文成绩数据,格式为字典,其中字典key为学生姓名,value为学生成绩: 那么,如何获得单科成绩排名前3的学生姓名? 代码如下:--数据样例,方便测试 def dic ...

  6. python基本数据结构-字典-方法

  7. Python基本数据结构-字典-创建/访问/基本操作/格式化输出

  8. Python开发——数据结构【深浅拷贝】

    浅拷贝 # 浅拷贝只copy一层 s = [3,'Lucy',4,[1,2]] s1 = s.copy() 深拷贝 # 深拷贝——克隆一分 import copy s = [3,'Lucy',4,[1 ...

  9. Python开发【源码剖析】 Dict对象

    static void ShowDictObject(PyDictObject* dictObject) { PyDictEntry* entry = dictObject->ma_table; ...

随机推荐

  1. [转]linux下释放文件内存

    在Linux系统下,我们一般不需要去释放内存,因为系统已经将内存管理的很好.但是凡事也有例外,有的时候内存会被缓存占用掉,导致系统使用SWAP空间影响性能,此时就需要执行释放内存(清理缓存)的操作了. ...

  2. 说说C与汇编之间的互相联系(转)

    在嵌入式系统开发中,目前使用的主要编程语言是C和汇编,C++已经有相应的编译器,但是现在使用还是比较少的.在稍大规模的嵌入式软件中,例如含有OS,大部分的代码都是用C编写的,主要是因为C语言的结构比较 ...

  3. socket.io websocket

    不能不知道的事: 在Http协议中,客户端向服务器端发送请求,服务器端收到请求再进行回应,整个过程中,服务器端是被动方,客户端是主动方: websoket是H5的一种基于TCP的新通信协议,它与Htt ...

  4. iOS - UICollectionView 瀑布流 添加表头视图的坑

    UICollectionView 瀑布流 添加表头视图的坑 首先是,需求加了个头视图在顶部,在collectionView中的头视图跟TableView的不一样,TableView的表头只要设置tab ...

  5. 关于PHP中的 serialize () 和 unserialize () 的使用(即关于PHP中的值与已存储的表示的相互转换)

    有时,我们会碰到这样的数据(字符串) 1 a:3:{i:0;s:44:"/Uploads/images/2017-07-21/5971a9a08ad57.png";i:1;s:44 ...

  6. 【架构师之路】APP架构师必看:面对爆发流量如何进行架构调整

    一.APP架构与WEB架构的最大不同 移动APP的架构和传统PC的WEB架构有三点不同: 1.连接的稳定性.在传统的web端连接成功后就可以认为它是稳定的,但在移动端.无线端,APP连接非常敏感,可能 ...

  7. 【抓包分析】Charles和 夜神模拟器 对安卓应用进行抓包分析

    准备工具 : 1 Charles   : https://www.charlesproxy.com  (收费) 2 夜神模拟器  : https://www.yeshen.com  (免费) 2 模拟 ...

  8. 修改计算机名或IP后Oracle10g无法启动服务的解决办法

    修改计算机名或IP后Oracle10g无法启动服务的解决办法 遇到的问题,问题产生原因不详.症状为,windows服务中有一项oracle服务启动不了,报出如下错误. Windows 不能在 本地计算 ...

  9. 【大数据系列】节点的退役和服役[datanode,yarn]

    一.datanode添加新节点 1 在dfs.include文件中包含新节点名称,该文件在名称节点的本地目录下 [白名单] [s201:/soft/hadoop/etc/hadoop/dfs.incl ...

  10. Linux命令 free:查看内存使用情况