Python字典对象实现原理详解

字典类型是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是一种类似的伪删除方式,保证探测链的连续性。

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)
}

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

(0)

相关推荐

  • Python3实现的字典、列表和json对象互转功能示例

    本文实例讲述了Python3实现的字典.列表和json对象互转功能.分享给大家供大家参考,具体如下: python3可以使用json模块操作json json.dumps(): 对json进行编码,对应php的json_encode() json.loads(): 对json进行解码,对应php的json_decode() test.py #!/usr/bin/python3 import json #python字典类型转换为json对象 data = { 'id' : 1, 'name' :

  • python使用参数对嵌套字典进行取值的方法

    因一些特殊需求需要以参数的形式获取字典中特定的值,网上搜了一下并没有特别好的实现(并没有太认真去找~),所以自己实现了一个,以供大家参考:) . 话不多说,直接上代码: def dict_get(dic, locators, default=None): ''' :param dic: 输入需要在其中取值的原始字典 <dict> :param locators: 输入取值定位器, 如:['result', 'msg', '-1', 'status'] <list> :param d

  • Python字典数据对象拆分的简单实现方法

    本文实例讲述了Python字典数据对象拆分的简单实现方法.分享给大家供大家参考,具体如下: 有朋友问了下问题: {'A1;A2': 'B','A3': 'C'}这种数据结构要拆解成{'A1':'B', 'A2': 'B', 'A3': 'C'},要如何实现? 这种问题,如果用普通的for循环来实现的话,还是有点麻烦: >>> dct = {'A1;A2': 'B','A3': 'C'} >>> tmp = {} >>> for k,v in dct.i

  • python字典改变value值方法总结

    今天这篇文章中我们来了解一下python之中的字典,在这文章之中我会对python字典修改进行说明,以及举例说明如何修改python字典内的值.废话不多说,我们开始进入文章吧. 首先我们得知道什么是修改字典 修改字典 向字典添加新内容的方法是增加新的键/值对,修改或删除已有键/值对如下实例: # !/usr/bin/python dict = {'Name': 'Zara', 'Age': 7, 'Class': 'First'}; dict['Age'] = 8; # update exist

  • python实现class对象转换成json/字典的方法

    本文实例讲述了python实现class对象转换成json字典的方法.分享给大家供大家参考,具体如下: # -*- encoding: UTF-8 -*- class Student: name = '' age = 0 def __init__(self, name, age): self.name = name self.age = age def convert_to_dict(obj): '''把Object对象转换成Dict对象''' dict = {} dict.update(obj

  • python3 json数据格式的转换(dumps/loads的使用、dict to str/str to dict、json字符串/字典的相互转换)

    python3 json数据格式的转换(dumps/loads的使用.dict to str/str to dict.json字符串/字典的相互转换) Python3 JSON 数据解析 JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式.它基于ECMAScript的一个子集. Python3 中可以使用 json 模块来对 JSON 数据进行编解码,它包含了两个函数: json.dumps(): 对数据进行编码. json.loads(): 对数据进

  • Python字典对象实现原理详解

    字典类型是Python中最常用的数据类型之一,它是一个键值对的集合,字典通过键来索引,关联到相对的值,理论上它的查询复杂度是 O(1) : >>> d = {'a': 1, 'b': 2} >>> d['c'] = 3 >>> d {'a': 1, 'b': 2, 'c': 3} 在字符串的实现原理文章中,曾经出现过字典对象用于intern操作,那么字典的内部结构是怎样的呢?PyDictObject对象就是dict的内部实现. 哈希表 (HASH TA

  • Python字典底层实现原理详解

    在Python中,字典是通过散列表或说哈希表实现的.字典也被称为关联数组,还称为哈希数组等.也就是说,字典也是一个数组,但数组的索引是键经过哈希函数处理后得到的散列值.哈希函数的目的是使键均匀地分布在数组中,并且可以在内存中以O(1)的时间复杂度进行寻址,从而实现快速查找和修改.哈希表中哈希函数的设计困难在于将数据均匀分布在哈希表中,从而尽量减少哈希碰撞和冲突.由于不同的键可能具有相同的哈希值,即可能出现冲突,高级的哈希函数能够使冲突数目最小化.Python中并不包含这样高级的哈希函数,几个重要

  • Python字符串对象实现原理详解

    在Python世界中将对象分为两种:一种是定长对象,比如整数,整数对象定义的时候就能确定它所占用的内存空间大小,另一种是变长对象,在对象定义时并不知道是多少,比如:str,list, set, dict等. >>> import sys >>> sys.getsizeof(1000) 28 >>> sys.getsizeof(2000) 28 >>> sys.getsizeof("python") 55 >&

  • Python整数对象实现原理详解

    整数对象在Python内部用PyIntObject结构体表示: typedef struct { PyObject_HEAD long ob_ival; } PyIntObject; PyObject_HEAD宏中定义的两个属性分别是: int ob_refcnt; struct _typeobject *ob_type; 这两个属性是所有Python对象固有的: ob_refcnt:对象的引用计数,与Python的内存管理机制有关,它实现了基于引用计数的垃圾收集机制 ob_type:用于描述P

  • Python列表对象实现原理详解

    Python中的列表基于PyListObject实现,列表支持元素的插入.删除.更新操作,因此PyListObject是一个变长对象(列表的长度随着元素的增加和删除而变长和变短),同时它还是一个可变对象(列表中的元素根据列表的操作而发生变化,内存大小动态的变化),PyListObject的定义: typedef struct { # 列表对象引用计数 int ob_refcnt; # 列表类型对象 struct _typeobject *ob_type; # 列表元素的长度 int ob_siz

  • Python图像处理之边缘检测原理详解

    目录 原理 Sobel检测算子 Laplacian算子 算子比较 原理 边缘检测是图像处理和计算机视觉当中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点,图像的边缘检测可以大幅度的减少数据量,并且剔除了可以认为不相关的信息,保留了图像重要的结构属性,它们绝大多数可以分为两类:基于搜索和基于零穿越. 基于搜索:通过寻找图像一阶导数中max来检测边界,然后利用计算结果估计边缘的局部方向,通常采用梯度的方向,并在此方向找到局部梯度模的最大值,代表的算法是Sobel算子和Scharr算子.

  • python super用法及原理详解

    这篇文章主要介绍了python super用法及原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 概念 super作为python的内建函数.主要作用如下: 允许我们避免使用基类 跟随多重继承来使用 实例 在单个继承的场景下,一般使用super来调用基类来实现: 下面是一个例子: class Mammal(object): def __init__(self, mammalName): print(mammalName, 'is a wa

  • Python模块future用法原理详解

    这篇文章主要介绍了Python模块future用法原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 计算机的知识太多了,很多东西就是一个使用过程中详细积累的过程.最近遇到了一个很久关于future的问题,踩了坑,这里就做个笔记,免得后续再犯类似错误. future的作用:把下一个新版本的特性导入到当前版本,于是我们就可以在当前版本中测试一些新版本的特性.说的通俗一点,就是你不用更新python的版本,直接加这个模块,就可以使用python

  • Python日志syslog使用原理详解

    这篇文章主要介绍了Python日志syslog使用原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 syslog的官方说明在: https://docs.python.org/2/library/syslog.html#module-syslog 该模块的主要方式为: #!/usr/bin/python # -*- coding: utf-8 -*- import syslog syslog.openlog([ident[, logopt

  • Python定时器线程池原理详解

    这篇文章主要介绍了Python定时器线程池原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 定时器执行循环任务: 知识储备 Timer(interval, function, args=None, kwargs=None) interval ===> 时间间隔 单位为s function ===> 定制执行的函数 使用threading的 Timer 类 start() 为通用的开始执行方法 cancel ()为取消执行的方法 普通单次

随机推荐