字典内部剖析

开篇先提出几个疑问:

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

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

键值要求:

在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),其代码如下:

  1. for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {
  2.   i = (i << 2) + i + perturb + 1;
  3. ep = &ep0[i & mask];

  PERTURB_SHIFT 默认值为5 ,每次循环perturb(散列值)进行位运算,与mask(数组长度-1)进行取余运算,不断获取新的下标,直到获取可用的表元

说了这么多,你可能恍然大悟,原来是这样的!! 当然更多的估计还是一知半解,那么下面直接抛源码,开干!

CPython源码剖析

  更多介绍可以参阅dictobject.c的源码

关联容器的entry(表元)

  我们将把关联容器的一个(键,值)元素对称为一个entry或slot。在Python中,一个entry的定义如下:

  1. typedef struct{
  2. Py_ssize_t me_hash; # key的散列
  3. PyObject *me_key; # 键
  4. PyObject *me_value; # 值
  5. }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. HttpClient(二)-- 模拟浏览器抓取网页

    一.设置请求头消息 User-Agent模拟浏览器 1.当使用第一节的代码 来 访问推酷的时候,会返回给我们如下信息: 网页内容:<!DOCTYPE html> <html> ...

  2. VS调试DLL项目代码

    如果DLL有对应的lib文件,并且dll工程和调用它的exe属于同一个解决方案,直接打断点调试就可以.例如OSG解决方案中的例子程序可以直接调试osgUtil模块的代码. 如果A.exe项目和A.dl ...

  3. MHL相关资源链接

    http://www.mhlconsortium.org/ 消费者网站: www.meetmhl.com采用者网站:www.mhltech.org 博客http://blog.sina.com.cn/ ...

  4. Android--Led_Demo_APK控制LED灯

    下面代码主要实现接口定义,实现从.so库文件接口函数在JAVA里面的声明:package com.friendlyarm.AndroidSDK; import android.util.Log; pu ...

  5. oracle数据据 Python+Pandas 获取Oracle数据库并加入DataFrame

    import pandas as pd import sys import imp imp.reload(sys) from sqlalchemy import create_engine impor ...

  6. N76E003之IO控制

    N76E003最多支持26个可位寻址的通用I/O引脚,分成4组 P0 到 P3 .每一个端口有它的端口控制寄存器(Px).端口控制寄存器的写和读有不同的意思.写端口控制寄存器设置输出锁存逻辑值,读端口 ...

  7. CDN的那些细枝末节

    起源: 原本打算系统看看关于axios的介绍,无意中就看见一句"Using cdn",于是百度一下,"cdn"是什么? 名词解释:CDN CDN的全称是Cont ...

  8. 【云迁移论文笔记】A Comparison of On-premise to Cloud Migration Approaches

    A Comparison of On-premise to Cloud Migration Approaches Author Info: Claus Pahl senior lecturer at ...

  9. 【Spring Boot&&Spring Cloud系列】Spring Boot配置文件

    很多的参数可以配置在application.properties或application.yml文件中 一.BANNER banner.charset=UTF-8 # Banner file enco ...

  10. vue - 父组件数据变化控制子组件类名切换

    先说当时的思路和实现核心是父子组件传值和v-bind指令动态绑定class实现 1. 父组件引用.注册.调用子组件script中引用 import child from '../components/ ...