Python字典对象实现原理详解


Posted in Python onJuly 01, 2019

字典类型是Python中最常用的数据类型之一,它是一个键值对的集合,字典通过键来索引,关联到相对的值,理论上它的查询复杂度是 O(1) :

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

在字符串的实现原理文章中,曾经出现过字典对象用于intern操作,那么字典的内部结构是怎样的呢?PyDictObject对象就是dict的内部实现。

哈希表 (HASH TABLES)

哈希表(也叫散列表),根据关键值对(Key-value)而直接进行访问的数据结构。它通过把key和value映射到表中一个位置来访问记录,这种查询速度非常快,更新也快。而这个映射函数叫做哈希函数,存放值的数组叫做哈希表。 哈希函数的实现方式决定了哈希表的搜索效率。具体操作过程是:

1.数据添加:把key通过哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。

2.数据查询:再次使用哈希函数将key转换为对应的数组下标,并定位到数组的位置获取value。

但是,对key进行hash的时候,不同的key可能hash出来的结果是一样的,尤其是数据量增多的时候,这个问题叫做哈希冲突。如果解决这种冲突情况呢?通常的做法有两种,一种是链接法,另一种是开放寻址法,Python选择后者。

开放寻址法(OPEN ADDRESSING)

开放寻址法中,所有的元素都存放在散列表里,当产生哈希冲突时,通过一个探测函数计算出下一个候选位置,如果下一个获选位置还是有冲突,那么不断通过探测函数往下找,直到找个一个空槽来存放待插入元素。

PYDICTENTRY

字典中的一个key-value键值对元素称为entry(也叫做slots),对应到Python内部是PyDictEntry,PyDictObject就是PyDictEntry的集合。PyDictEntry的定义是:

typedef struct {
/* Cached hash code of me_key. Note that hash codes are C longs.
* We have to use Py_ssize_t instead because dict_popitem() abuses
* me_hash to hold a search finger.
*/
Py_ssize_t me_hash;
PyObject *me_key;
PyObject *me_value;
} PyDictEntry;

me_hash用于缓存me_key的哈希值,防止每次查询时都要计算哈希值,entry有三种状态。

1.Unused: me_key == me_value == NULL

Unused是entry的初始状态,key和value都为NULL。插入元素时,Unused状态转换成Active状态。这是me_key为NULL的唯一情况。

2. Active: me_key != NULL and me_key != dummy 且 me_value != NULL

插入元素后,entry就成了Active状态,这是me_value唯一不为NULL的情况,删除元素时Active状态刻转换成Dummy状态。

3. Dummy: me_key == dummy 且 me_value == NULL

此处的dummy对象实际上一个PyStringObject对象,仅作为指示标志。Dummy状态的元素可以在插入元素的时候将它变成Active状态,但它不可能再变成Unused状态。

为什么entry有Dummy状态呢?这是因为采用开放寻址法中,遇到哈希冲突时会找到下一个合适的位置,例如某元素经过哈希计算应该插入到A处,但是此时A处有元素的,通过探测函数计算得到下一个位置B,仍然有元素,直到找到位置C为止,此时ABC构成了探测链,查找元素时如果hash值相同,那么也是顺着这条探测链不断往后找,当删除探测链中的某个元素时,比如B,如果直接把B从哈希表中移除,即变成Unused状态,那么C就不可能再找到了,因为AC之间出现了断裂的现象,正是如此才出现了第三种状态---Dummy,Dummy是一种类似的伪删除方式,保证探测链的连续性。

Python字典对象实现原理详解

PYDICTOBJECT

PyDictObject就是PyDictEntry对象的集合,PyDictObject的结构是:

typedef struct _dictobject PyDictObject;
struct _dictobject {
PyObject_HEAD
Py_ssize_t ma_fill; /* # Active + # Dummy */
Py_ssize_t ma_used; /* # Active */
/* The table contains ma_mask + 1 slots, and that's a power of 2.
* We store the mask instead of the size because the mask is more
* frequently needed.
*/
Py_ssize_t ma_mask;
/* ma_table points to ma_smalltable for small tables, else to
* additional malloc'ed memory. ma_table is never NULL! This rule
* saves repeated runtime null-tests in the workhorse getitem and
* setitem calls.
*/
PyDictEntry *ma_table;
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
};
  • ma_fill :所有处于Active以及Dummy的元素个数
  • ma_used :所有处于Active状态的元素个数
  • ma_mask :所有entry的元素个数(Active+Dummy+Unused)
  • ma_smalltable:创建字典对象时,一定会创建一个大小为PyDict_MINSIZE==8的PyDictEntry数组。
  • ma_table:当entry数量小于PyDict_MINSIZE,ma_table指向ma_smalltable的首地址,当entry数量大于8时,Python把它当做一个大字典来处理,此刻会申请额外的内存空间,同时将ma_table指向这块空间。
  • ma_lookup:字典元素的搜索策略

PyDictObject使用PyObject_HEAD而不是PyObject_Var_HEAD,虽然字典也是变长对象,但此处并不是通过ob_size来存储字典中元素的长度,而是通过ma_used字段。

PYDICTOBJECT的创建过程

PyObject *
PyDict_New(void)
{
register PyDictObject *mp;
if (dummy == NULL) { /* Auto-initialize dummy */
dummy = PyString_FromString("<dummy key>");
if (dummy == NULL)
return NULL;
}
if (numfree) {
mp = free_list[--numfree];
assert (mp != NULL);
assert (Py_TYPE(mp) == &PyDict_Type);
_Py_NewReference((PyObject *)mp);
if (mp->ma_fill) {
EMPTY_TO_MINSIZE(mp);
} else {
/* At least set ma_table and ma_mask; these are wrong
if an empty but presized dict is added to freelist */
INIT_NONZERO_DICT_SLOTS(mp);
}
assert (mp->ma_used == 0);
assert (mp->ma_table == mp->ma_smalltable);
assert (mp->ma_mask == PyDict_MINSIZE - 1);
} else {
mp = PyObject_GC_New(PyDictObject, &PyDict_Type);
if (mp == NULL)
return NULL;
EMPTY_TO_MINSIZE(mp);
}
mp->ma_lookup = lookdict_string;
return (PyObject *)mp;
}
  • 初始化dummy对象
  • 如果缓冲池还有可用的对象,则从缓冲池中读取,否则,执行步骤3
  • 分配内存空间,创建PyDictObject对象,初始化对象
  • 指定添加字典元素时的探测函数,元素的搜索策略

字典搜索策略

static PyDictEntry *
lookdict(PyDictObject *mp, PyObject *key, register long hash)
{
register size_t i;
register size_t perturb;
register PyDictEntry *freeslot;
register size_t mask = (size_t)mp->ma_mask;
PyDictEntry *ep0 = mp->ma_table;
register PyDictEntry *ep;
register int cmp;
PyObject *startkey;

i = (size_t)hash & mask;
ep = &ep0[i];
if (ep->me_key == NULL || ep->me_key == key)
return ep;

if (ep->me_key == dummy)
freeslot = ep;
else {
if (ep->me_hash == hash) {
startkey = ep->me_key;
Py_INCREF(startkey);
cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
Py_DECREF(startkey);
if (cmp < 0)
return NULL;
if (ep0 == mp->ma_table && ep->me_key == startkey) {
if (cmp > 0)
return ep;
}
else {
/* The compare did major nasty stuff to the
* dict: start over.
* XXX A clever adversary could prevent this
* XXX from terminating.
*/
return lookdict(mp, key, hash);
}
}
freeslot = NULL;
}

/* In the loop, me_key == dummy is by far (factor of 100s) the
least likely outcome, so test for that last. */
for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {
i = (i << 2) + i + perturb + 1;
ep = &ep0[i & mask];
if (ep->me_key == NULL)
return freeslot == NULL ? ep : freeslot;
if (ep->me_key == key)
return ep;
if (ep->me_hash == hash && ep->me_key != dummy) {
startkey = ep->me_key;
Py_INCREF(startkey);
cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
Py_DECREF(startkey);
if (cmp < 0)
return NULL;
if (ep0 == mp->ma_table && ep->me_key == startkey) {
if (cmp > 0)
return ep;
}
else {
/* The compare did major nasty stuff to the
* dict: start over.
* XXX A clever adversary could prevent this
* XXX from terminating.
*/
return lookdict(mp, key, hash);
}
}
else if (ep->me_key == dummy && freeslot == NULL)
freeslot = ep;
}
assert(0); /* NOT REACHED */
return 0;
}

字典在添加元素和查询元素时,都需要用到字典的搜索策略,搜索时,如果不存在该key,那么返回Unused状态的entry,如果存在该key,但是key是一个Dummy对象,那么返回Dummy状态的entry,其他情况就表示存在Active状态的entry,那么对于字典的插入操作,针对不同的情况进行操作也不一样。对于Active的entry,直接替换me_value值即可;对于Unused或Dummy的entry,需要同时设置me_key,me_hash和me_value

PYDICTOBJECT对象缓冲池

PyDictObject对象缓冲池和PyListObject对象缓冲池的原理是类似的,都是在对象被销毁的时候把该对象添加到缓冲池中去,而且值保留PyDictObject对象本身,如果ma_table维护的时从系统堆中申请的空间,那么Python会释放这块内存,如果ma_table维护的是ma_smalltable,那么只需把smalltable中的元素的引用计数减少即可。

static void
dict_dealloc(register PyDictObject *mp)
{
register PyDictEntry *ep;
Py_ssize_t fill = mp->ma_fill;
PyObject_GC_UnTrack(mp);
Py_TRASHCAN_SAFE_BEGIN(mp)
for (ep = mp->ma_table; fill > 0; ep++) {
if (ep->me_key) {
--fill;
Py_DECREF(ep->me_key);
Py_XDECREF(ep->me_value);
}
}
if (mp->ma_table != mp->ma_smalltable)
PyMem_DEL(mp->ma_table);
if (numfree < PyDict_MAXFREELIST && Py_TYPE(mp) == &PyDict_Type)
free_list[numfree++] = mp;
else
Py_TYPE(mp)->tp_free((PyObject *)mp);
Py_TRASHCAN_SAFE_END(mp)
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
简单解析Django框架中的表单验证
Jul 17 Python
python 队列详解及实例代码
Oct 18 Python
Python MD5加密实例详解
Aug 02 Python
Python实现定时精度可调节的定时器
Apr 15 Python
Python类和对象的定义与实际应用案例分析
Dec 27 Python
python 调用有道api接口的方法
Jan 03 Python
如何通过雪花算法用Python实现一个简单的发号器
Jul 03 Python
python判断一个对象是否可迭代的例子
Jul 22 Python
python调用接口的4种方式代码实例
Nov 19 Python
python二分法查找算法实现方法【递归与非递归】
Dec 06 Python
keras在构建LSTM模型时对变长序列的处理操作
Jun 29 Python
在vscode中启动conda虚拟环境的思路详解
Dec 25 Python
Python Pandas 获取列匹配特定值的行的索引问题
Jul 01 #Python
Python动态语言与鸭子类型详解
Jul 01 #Python
详解python websocket获取实时数据的几种常见链接方式
Jul 01 #Python
使用python将mysql数据库的数据转换为json数据的方法
Jul 01 #Python
python字符串Intern机制详解
Jul 01 #Python
简单了解python单例模式的几种写法
Jul 01 #Python
python如何获取列表中每个元素的下标位置
Jul 01 #Python
You might like
一些使用频率比较高的php函数
2008/10/03 PHP
php rmdir使用递归函数删除非空目录实例详解
2016/10/20 PHP
php生成网页桌面快捷方式
2017/05/05 PHP
Laravel框架模板加载,分配变量及简单路由功能示例
2018/06/11 PHP
PHP删除数组中指定值的元素常用方法实例分析【4种方法】
2018/08/21 PHP
PHP5.6.8连接SQL Server 2008 R2数据库常用技巧分析总结
2019/05/06 PHP
javascript断点调试心得分享
2016/04/23 Javascript
JavaScript中判断数据类型的方法总结
2016/05/24 Javascript
jQuery学习笔记之入门
2016/12/14 Javascript
原生Aajax 和jQuery Ajax 写法个人总结
2017/03/24 jQuery
详解在vue-cli项目中安装node-sass
2017/06/21 Javascript
Vue中的数据监听和数据交互案例解析
2017/07/12 Javascript
360doc网站不登录就无法复制内容的解决方法
2018/01/27 Javascript
使用vue2实现购物车和地址选配功能
2018/03/29 Javascript
javascript实现贪吃蛇小游戏
2020/07/28 Javascript
JavaScript实现跟随鼠标移动的盒子
2021/01/28 Javascript
[46:10]2014 DOTA2国际邀请赛中国区预选赛 CnB VS HGT
2014/05/21 DOTA
[01:03:27]Optic vs VGJ.S 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/20 DOTA
Python3中的2to3转换工具使用示例
2015/06/12 Python
python实现决策树分类算法
2017/12/21 Python
tensorflow 1.0用CNN进行图像分类
2018/04/15 Python
快速解决pandas.read_csv()乱码的问题
2018/06/15 Python
使用tensorflow实现线性svm
2018/09/07 Python
python利用Tesseract识别验证码的方法示例
2019/01/21 Python
django 做 migrate 时 表已存在的处理方法
2019/08/31 Python
Pytorch实现各种2d卷积示例
2019/12/30 Python
PyTorch加载自己的数据集实例详解
2020/03/18 Python
BeautifulSoup中find和find_all的使用详解
2020/12/07 Python
HTML5中的autofocus(自动聚焦)属性介绍
2014/04/23 HTML / CSS
Bata印度官网:源自欧洲舒适鞋履品牌
2020/01/30 全球购物
八项规定自查自纠报告及整改措施
2014/10/26 职场文书
2015年党员干部承诺书
2015/01/21 职场文书
申请吧主发表的感言
2015/08/03 职场文书
优秀毕业生主要事迹材料
2015/11/04 职场文书
2016反腐倡廉警示教育心得体会
2016/01/13 职场文书
让JavaScript代码更加精简的方法技巧
2022/06/01 Javascript