字典内部剖析

开篇先提出几个疑问:

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

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

键值要求:

在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. Weblogic(CVE-2017-10271)漏洞复现

    WebLogic XMLDecoder反序列化漏洞(CVE-2017-10271) 漏洞编号:CVE-2017-10271 漏洞描述:WebLogic WLS组件中存在CVE-2017-10271远程 ...

  2. java.util.concurrent.RejectedExecutionException 线程池饱和

    java.util.concurrent.RejectedExecutionException at java.util.concurrent.ThreadPoolExecutor$AbortPoli ...

  3. centos6.4搭建ftp服务器

    在centos环境下搭建ftp服务器,选择用vsftpd. 1.检测是否已经安装vsftpd # rpm -qa | grep vsftpd 如果已经安装vsftpd,会显示相应版本号.没有任何提示, ...

  4. 深入浅出MFC——Document-View深入探讨(五)

    1. MFC之所以为Application Framework,最重要的一个特征就是它能够将管理数据的程序代码和负责数据显示的程序代码分离开来,这种能力由MFC的Document/View提供.Doc ...

  5. linux sumba服务器简单配置

    使用samba设置linux和windows直接简单的文件共享 前提: 1.linux和windows已经可以互相ping同 2.已经安装好smb 查看是否安装smb rpm -aq|grep smb ...

  6. 《Lua程序设计》第7章 迭代器与泛型for 学习笔记

    本章将介绍如何编写适用于泛型for的迭代其(Iterator).7.1 迭代器与closurehttp://www.cnblogs.com/moonlightpoet/p/5685275.html 7 ...

  7. 【Java并发编程四】关卡

    一.什么是关卡? 关卡类似于闭锁,它们都能阻塞一组线程,直到某些事件发生. 关卡和闭锁关键的不同在于,所有线程必须同时到达关卡点,才能继续处理.闭锁等待的是事件,关卡等待的是其他线程. 二.Cycli ...

  8. 【技术分享会】 @第四期 JQuery插件

    本讲内容 JavaScript JQuery JQuery插件 实例 JavaScript 前端开发工程师必须掌握的三种技能 描述内容的HTML 描述网页样式的CSS 描述网页行为的JavaScrip ...

  9. read by other session 等待事件。

    今天是2014-01-06,从今天开始,打算春节之前每天学习一个等待事件,今天就记录一下read by other session这个等待事件笔记. 什么是read by other session? ...

  10. JavaScript 浮点数陷阱及解法

    众所周知,JavaScript 浮点数运算时经常遇到会 0.000000001 和 0.999999999 这样奇怪的结果,如 0.1+0.2=0.30000000000000004.1-0.9=0. ...