6.0 序

元素和元素之间可能存在着某种关系,比如学生姓名和成绩。我希望能够通过学生的姓名找到这个学生的成绩,那么只需要将两者关联起来即可。字典正是这么做的,字典中的每个元素就是一个key:value键值对,通过指定的key可以找到value。首先我们在前面的章节中说过,字典这种数据结构,python底层也在大量的使用,比如每一个类都有自己的属性字典,这就意味着python对字典这种数据结构的性能要求是极其苛刻的。所以在python底层,对字典这种数据结构进行了高度的优化。理论上,字典查找元素的时间复杂度是O(1)。

字典底层对应的结构体是PyDictObject,其实我不说,也能猜出来。再比如set,那么底层对应的结构体显然是PySetObject。我们先不看PyDictObject,我们来想一想为什么字典的查找效率是O(1),它底层是使用了什么原理。

6.1 哈希表

我们在tuple那一章中提到了哈希,还说tuple可以作为字典的key,list不可以,就是因为list是不可哈希的。没错,dict底层正是使用了哈希表,哈希表也叫做散列表。它是将值通过hash运算转为一个数值,这个数值来充当索引。这样解释可能会让人很迷,我们来具体看一张图。

《python解释器源码剖析》第6章--python中的dict对象-LMLPHP

我们发现除了key、value之外,还有一个index。其实hash表本质上也是使用了索引的思想,会把这个key通过函数映射成一个数值,作为索引。至于是怎么映射的,可以的话后面再谈,现在我们就假设是按照我们接下来说的方法映射的。

比如我们这里有一个能容纳10个元素的字典,我们先设置d["satori"]=82,那么会对satori这个字符串进行一个哈希运算,然后再对10、也就是当前的总容量取模,这样的是不是能够得到一个小于10的数呢?假设是5,那么就存在索引为5地方。然后又进行d["koishi"]=83,那么按照同样的规则运算得到8,那么就存在索引为8的位置,同理第三次设置d["mashiro"]=80,对mashiro进行哈希、取模,得到2,那么存储在索引为2的地方。

同理,当我们取值的时候,取d["satori"],那么同样会对satori进行哈希、取模,得到索引,发现是5,直接把索引为5的value给取出来。当然这种说法肯定是不严谨的,为什么我们来想一个问题。

  • 哈希、取模运算之后得到的结果一定是不同的吗?
  • 在运算得到索引的时候,发现这个位置已经有人占了怎么办?
  • 取值的时候,索引为5,可如果索引为5对应的key和我们指定获取的key不一致怎么办?

哈希值是有冲突的,如果一旦冲突,那么python底层会改变算法继续映射,直到映射出来的索引没有人用。比如我们设置一个新的key、value,d["tomoyo"]=88,可是我们对tomoyo这个key进行映射之后得到的结果也是5,而索引为5的地方已经被key=satori的键值对给占了,那么python就会换一种规则来对tomoyo进行hash运算,然后添加进去。但如果我们再次设置d["satori"]=100,那么对satori进行映射得到的结果也是5,而key是一致的,那么就会把对应的值进行修改。

同理,当我们获取值的时候,d["tomoyo"],对key进行映射,得到索引,但是发现key不是我们指定的key,于是改变规则(这个规则跟设置值冲突时,采用的规则是一样的),重新映射,得到索引,然后发现key是一致的,于是将值取出来。

但如果我们指定了一个不存在的key,那么哈希映射,找到对应索引,发现没有key,证明我们指定的key是不存在的。但如果有的话,发现key和我们指定的key不相等,说明我们只是碰巧撞上了,但由于key不一样,因此会改变规则重新运算,得到新的索引,发现没有对应的key,于是报错:指定的key不存在。

所以从这里就已经能说明问题了,就是把key转换成类似列表的索引。可能有人问,这些值貌似不是连续的啊,对的,肯定不是连续的。并不是说你先存,你的索引就小、就在前面,这是由key进行hash映射之后的结果决定的。而且容量有10个,目前我们只存了4个元素,那么哈希表、或者说字典会不会扩容呢?当然,既然是可变对象,当然会扩容。并且它还不是像列表那样,容量不够才扩容,而当元素个数达到容量的三分之二的时候就会扩容。

我们可以认为字典底层还是使用了索引的思想,字典不可能会像列表那样,元素之间是连续的,一个一个挨在一起的。既然是哈希运算,得到的值肯定是随机的。容量为10,尽管有6个是空着的,但是没关系,我只要保证我设置的元素整体上是有序的即可。就好比有10张桌椅,小红坐在第3张,小明坐在第8张,尽管有空着的,但是没关系,就让它空着。只要我到第3张桌椅能够找到小红、第8张可以找到小明即可。这些桌椅就可以看成是索引,只要我通过索引能够找到对应的元素即可。但是容量为10,为什么不能全部占满之后再扩容呢?试想一下,既然是随机的,那么肯定会出现哈希值碰撞,并且当元素个数到达三分之二之后,这种碰撞的概率非常大。因此当容量到达三分之二的时候,就会申请一份更大的空间,以便来容纳新的元素。

所以我们发现哈希表实际上就是一种空间换时间的方法,如果容量为100,那么就相当于有100个位置,每个元素都进行哈希映射,找到自己的位置。各自的位置都是不固定的,也许会空出来很多元素,但是无所谓,只要保证这些元素在100个位置上是相对有序、通过哈希运算得到索引之后,可以在相应的位置找到它即可。

所以相信应该所有人都能明白为什么哈希表的时间复杂度是O(1)了,就实际因为转化成了索引,每一个索引都是连续的,只不过一部分索引没有相应的key、value罢了。但这无所谓,因为索引和key、value是一一对应的,通过索引我们能瞬间定位到指定的key,再来检测key是否存在以及和我们指定的key是否一致。如果不存在,那么不好意思,证明这个地方根本没有key、value,说明我们指定了一个不存在的key。而且由于元素个数达到容量的三分之二的时候,碰撞的概率非常大,因此几乎不可能出现容量正好都排满的情况,否则那要改变规则、重复映射多少次啊。

一句话总结:哈希表就是一种空间换时间的方法

关于哈希表设置元素、和获取元素用流程图表示的话,就是:

《python解释器源码剖析》第6章--python中的dict对象-LMLPHP

《python解释器源码剖析》第6章--python中的dict对象-LMLPHP

6.2 PyDictObject对象

字典中的一个key、value,我们在底层会把它称之为一个entry,至于为什么?我们后面在源码中可以看到

typedef struct _dictkeysobject PyDictKeysObject;

/* The ma_values pointer is NULL for a combined table
* or points to an array of PyObject* for a split table
对于一张combined table,ma_values指针为NULL
对于一张split table,则指向一个数组,数组里面都是PyObject *
*/
typedef struct {
//注意这是PyObject_HEAD,不是PyObject_VAR_HEAD
//PyObject_HEAD只有引用计数和类型,没有ob_size
PyObject_HEAD //字典里面元素的个数,active
Py_ssize_t ma_used; /* Dictionary version: globally unique, value change each time
the dictionary is modified
字典版本:全局唯一,每一次value的变动,都会导致其改变
*/
uint64_t ma_version_tag; /*
如果ma_values为NULL,这是一张combined table,所有的key和value都存在ma_keys里面
*/
PyDictKeysObject *ma_keys; /*
如果ma_values不为NULL,这是一张split table,那么key都存在ma_keys里
所有的values都存在ma_values这个数组里
*/
PyObject **ma_values;
} PyDictObject; //不管装在设么地方,我们看到存储的都是PyObject *
//说明字典是什么都可以装的(不可变类型)

但是说实话,直接这么看是很难看懂的,然而我们发现有一个PyDictKeysObject *,而这个家伙就是_dictkeysobject,从最上面的typedef struct也能看出来,我们来看看这个_dictkeysobject是什么吧

//Objects/dict-common.h
struct _dictkeysobject {
//引用计数
Py_ssize_t dk_refcnt; /* Size of the hash table (dk_indices). It must be a power of 2. */
/* 哈希表的大小,必须是2的倍数 */
Py_ssize_t dk_size; /* 与哈希表有关的函数 */
dict_lookup_func dk_lookup; /* Number of usable entries in dk_entries. */
/* dk_entries中可用的entries数量 */
Py_ssize_t dk_usable; /* Number of used entries in dk_entries. */
/* dk_entries中已经使用的entries数量 */
Py_ssize_t dk_nentries; /* Actual hash table of dk_size entries. It holds indices in dk_entries,
or DKIX_EMPTY(-1) or DKIX_DUMMY(-2). Indices must be: 0 <= indice < USABLE_FRACTION(dk_size). The size in bytes of an indice depends on dk_size: Dynamically sized, SIZEOF_VOID_P is minimum. */
//最终的哈希表,它存储了dk_entries的索引
//里面的类型是会随着dk_size的大小而变化的
/*
- 1 byte if dk_size <= 0xff (char*)
- 2 bytes if dk_size <= 0xffff (int16_t*)
- 4 bytes if dk_size <= 0xffffffff (int32_t*)
- 8 bytes otherwise (int64_t*)
*/
char dk_indices[]; /* char is required to avoid strict aliasing. */ /* "PyDictKeyEntry dk_entries[dk_usable];" array follows:
see the DK_ENTRIES() macro */
}; //我们一直提到了dk_entries,这又是个啥?
//dk_entries是一个数组,里面的元素类型是PyDictKeyEntry,就是一个一个的键值对
//所以我们把某个键值对称之为一个entry,它的大小可以用USABLE_FRACTION这个宏来获取
typedef struct {
/* me_key的哈希值,避免每次查询的时候都要重新建立 */
Py_hash_t me_hash;
//字典的key
PyObject *me_key;
//这个字段只对combined table有意义
/*
还记得ma_values吗?上面说了如果是combined table,那么key和value都会存在PyDictKeysObject *ma_keys里面,但如果是split table,那就只有key会存在PyDictKeysObject *ma_keys里面,也就是这里me_key,所以这里注释了:me_value这个字段只对combined table有意义。因为是split table的话,value都会存储在ma_values里面,而不是这里的me_value
*/
PyObject *me_value; /* This field is only meaningful for combined tables */
} PyDictKeyEntry;

因此可以看到字典的定义还是蛮复杂的,但是仔细分析还是可以看懂的。PyDictObject里面有一个ma_values,如果是combined table,那么这个值是为NULL,key和value是放在PyDictKeyEntry里面的,由me_key和me_value存储,这当然也是一个PyObject *指针类型。如果是split table,那么ma_values则是一个数组,存储所有value,当然这里的value也是指针,PyDictKeyEntry则只存储key。而哈希表还要对应一个索引啊,这个索引都是放在PyDictKeysObject里面的。

6.2.1 再谈哈希表

从6.1中,我们知道了哈希表的基本思想,就是通过某个函数将需要搜索的键值映射为一个索引,然后通过索引去访问连续的内存区域。而对于哈希表这种数据结构,最终目的就是加速键的搜索过程。而用于映射的函数就是哈希函数,映射之后的值就是哈希值。因此在哈希表的实现中,哈希函数的优劣将直接决定实现的哈希表的搜索效率的高低。

并且我们知道,当元素到达容量的三分之二的时候,会很容易出现哈希值冲突,我们之前说如果冲突了,就改变规则重新映射。事实上,python也确实是这么做的,这种方法叫做开放寻址法。

当发生哈希值冲突时,python会通过一个二次探测函数f,计算下一个候选位置addr,如果可用就插入进去。如果不可用,会继续使用探测函数,直到找到一个可用的位置。

通过多次使用探测函数f,从一个位置可以到达多个位置,我们认为这些位置形成了一个"冲突探测链(探测序列)",比如当我们插入一个key="satori"的键值对,在a位置发现不行,又走b位置,发现也被人占了,于是到达c位置,发现没有key,于是就占了c这个位置。但是问题来了,如果我此时把b位置上键值对给删掉会引发什么后果?首先我们知道,b位置上的key和我们指定的值为"satori"的key通过哈希函数映射出来的索引是一样的,当我们直接获取d["satori"],肯定会先走a位置,发现有人但key又不是"satori",于是重新映射,走到b,发现还不对,再走到c位置,发现key是"satori",于是就把值取出来了。但是,我要说但是了,如果我们把b位置上的元素删掉呢?那么老规矩,获取、映射、走到a发现坑被占、走到b结果发现居然没有内容,那么直接就报出了一个KeyError。继续寻找的前提是,这个地方要存储了key、value,并且存在的key和指定的key不相同,但如果没有的话,就说明根本没有这个key。然而呢?"satori"这个key确实是存在的,因此发生这种情况我们就说探测链断裂。本来应该走到c的,但是由于b没有元素,因此探测函数在b处就停止了

因此我们发现,当一个元素只要位于任何一条探测链当中,在删除元素时都不能真正意义上的删除,而是一种"伪删除"操作

6.2.2 entry的三种状态

还记得这个entry吗?对于字典里面的一个键值对就叫做一个entry

typedef struct {
Py_hash_t me_hash;
PyObject *me_key;
PyObject *me_value;
} PyDictKeyEntry;

在python中,当一个PyDictObject对象发生变化时,其中的entry会在三种不同的状态之间进行切换:unused态、active态、dummy态。

  • 当一个entry的me_key和me_value都是NULL的时候,entry处于unused态。unused态表明该entry中并没有存储key、value,并且在此之前也没有存储过它们。每一个entry在初始化的时候都会处于这个状态,me_value不管何时都可能会NULL,这取决于到底是combined table、还是split table,但是对于me_key,只可能在unused的时候才可能会NULL。
  • 当entry存储了key时,那么此时entry便从unused态变成了active态
  • 当entry中的key(value)被删除后,状态便从active态变成dummy态,注意:这里是dummy,删除了并不代表就能够回到unused态,来存储其他key了。我们也说了,unused态是指当前没有、并且之前也没有存储过。key被删除后,会变成dummy。否则就会发生我们之前说的探测链断裂,至于这个dummy到底是啥,我们后面说。总是entry进入dummy态,就是我们刚才提到的伪删除技术,当python沿着某条探测链搜索时,如果发现一个entry处于dummy态,就会明白虽然当前的entry是无效的,但是后面的entry可能是有效的,而不会直接就停止搜索、报错,这样就保证了探测链的连续性。至于报错,是在找到了unused状态的entry时才会报错,因为这里确实一直都没有存储过key,但是索引确实是这个位置,这说明当前指定的key就真的不存在哈希表中,此时才会报错。

《python解释器源码剖析》第6章--python中的dict对象-LMLPHP

6.3 PyDictObject的创建与维护

6.3.1 PyDictObject的创建

python内部通过PyDict_New来创建一个新的dict对象。

PyObject *
PyDict_New(void)
{
//new_keys_object表示创建PyDictKeysObject*对象
//里面传一个数值,表示entry的容量
//#define PyDict_MINSIZE 8,从宏定义我们能看出来为8
//表示默认初始化能容纳8个entry的PyDictKeysObject
//为什么是8,这是通过大量的经验得来的。
PyDictKeysObject *keys = new_keys_object(PyDict_MINSIZE);
if (keys == NULL)
return NULL;
//这一步则是根据PyDictKeysObject *创建一个新字典
return new_dict(keys, NULL);
} static PyDictKeysObject *new_keys_object(Py_ssize_t size)
{
PyDictKeysObject *dk;
Py_ssize_t es, usable; //检测,size是否>=PyDict_MINSIZE
assert(size >= PyDict_MINSIZE);
assert(IS_POWER_OF_2(size)); usable = USABLE_FRACTION(size);
//es:哈希表中的每个索引占多少字节
if (size <= 0xff) {
es = 1;
}
else if (size <= 0xffff) {
es = 2;
}
#if SIZEOF_VOID_P > 4
else if (size <= 0xffffffff) {
es = 4;
}
#endif
else {
es = sizeof(Py_ssize_t);
} //注意到,字典里面也有缓冲池,当然这里指定是字典的key
//如果有的话,直接从里面取
if (size == PyDict_MINSIZE && numfreekeys > 0) {
dk = keys_free_list[--numfreekeys];
}
else {
//否则malloc重新申请
dk = PyObject_MALLOC(sizeof(PyDictKeysObject)
+ es * size
+ sizeof(PyDictKeyEntry) * usable);
if (dk == NULL) {
PyErr_NoMemory();
return NULL;
}
}
//设置引用计数、可用的entry个数等信息
DK_DEBUG_INCREF dk->dk_refcnt = 1;
dk->dk_size = size;
dk->dk_usable = usable;
//dk_lookup很关键,里面包括了哈希函数和冲突时的二次探测函数的实现
dk->dk_lookup = lookdict_unicode_nodummy;
dk->dk_nentries = 0;
//哈希表的初始化
memset(&dk->dk_indices[0], 0xff, es * size);
memset(DK_ENTRIES(dk), 0, sizeof(PyDictKeyEntry) * usable);
return dk;
/*
keys.entries和values按照顺序
*/
} static PyObject *
new_dict(PyDictKeysObject *keys, PyObject **values)
{
PyDictObject *mp;
assert(keys != NULL);
//这是一个字典的缓冲池
if (numfree) {
mp = free_list[--numfree];
assert (mp != NULL);
assert (Py_TYPE(mp) == &PyDict_Type);
_Py_NewReference((PyObject *)mp);
}
//系统堆中申请内存
else {
mp = PyObject_GC_New(PyDictObject, &PyDict_Type);
if (mp == NULL) {
DK_DECREF(keys);
free_values(values);
return NULL;
}
}
//设置key、value等等
mp->ma_keys = keys;
mp->ma_values = values;
mp->ma_used = 0;
mp->ma_version_tag = DICT_NEXT_VERSION();
assert(_PyDict_CheckConsistency(mp));
return (PyObject *)mp;
}

6.3.2 PyDictObject的元素搜索

python为哈希表搜索提供了多种函数,lookdict、lookdict_unicode、lookdict_index,一般通用的是lookdict,lookdict_unicode则是专门针对key为unicode的entry,lookdict_index针对key为int的entry,可以把lookdict_unicode、lookdict_index看成lookdict的特殊实现,只不过这两种可以非常的常用,因此单独实现了一下。

注意:我们无论是对字典设置值还是获取值,都需要进行搜索策略。我们来看看lookdict的底层实现

static Py_ssize_t _Py_HOT_FUNCTION
lookdict(PyDictObject *mp, PyObject *key,
Py_hash_t hash, PyObject **value_addr)
{
size_t i, mask, perturb;
//keys数组的首地址
PyDictKeysObject *dk;
//entries数组的首地址
PyDictKeyEntry *ep0; top:
dk = mp->ma_keys;
ep0 = DK_ENTRIES(dk);
mask = DK_MASK(dk);
perturb = hash;
//哈希,定位探测链冲突的第一个entry的索引
i = (size_t)hash & mask; for (;;) {
// dk->indecs[i]
Py_ssize_t ix = dk_get_index(dk, i);
//如果ix == DKIX_EMPTY,说明没有存储值
//理论上是报错的,但是在底层是将值的指针设置为NULL
if (ix == DKIX_EMPTY) {
*value_addr = NULL;
return ix;
}
if (ix >= 0) {
//拿到指定的entry的指针
PyDictKeyEntry *ep = &ep0[ix];
assert(ep->me_key != NULL);
//如果两个key一样,那么直接将值的地址设置为ep->me_value
/*
但是注意这里的一样,相当于在python中,两个地址一样的对象
也就是说,a is b是为True
*/
if (ep->me_key == key) {
*value_addr = ep->me_value;
return ix;
}
//如果两个对象不一样,那么就比较它们的哈希值是否相同
//比如33和33是一个对象,但是3333和3333却不是,但是它们的值是一样的
//因此先判断id是否一致,如果不一致再比较值是否一样,当然这里是哈希值
if (ep->me_hash == hash) {
PyObject *startkey = ep->me_key;
Py_INCREF(startkey);
int cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
Py_DECREF(startkey);
if (cmp < 0) {
*value_addr = NULL;
return DKIX_ERROR;
}
if (dk == mp->ma_keys && ep->me_key == startkey) {
if (cmp > 0) {
*value_addr = ep->me_value;
return ix;
}
}
else {
/* The dict was mutated, restart */
goto top;
}
}
}
//如果条件均不满足,调整姿势,进行下一次探索
perturb >>= PERTURB_SHIFT;
i = (i*5 + perturb + 1) & mask;
}
Py_UNREACHABLE();
}

6.3.4 插入元素

我们对PyDictObject对象的操作都是建立在搜索的基础之上的,插入和删除也不例外。

static int
insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
{
PyObject *old_value;
PyDictKeyEntry *ep; //增加对key和value的引用计数
Py_INCREF(key);
Py_INCREF(value);
//类型检查
if (mp->ma_values != NULL && !PyUnicode_CheckExact(key)) {
if (insertion_resize(mp) < 0)
goto Fail;
} Py_ssize_t ix = mp->ma_keys->dk_lookup(mp, key, hash, &old_value);
if (ix == DKIX_ERROR)
goto Fail; assert(PyUnicode_CheckExact(key) || mp->ma_keys->dk_lookup == lookdict);
MAINTAIN_TRACKING(mp, key, value); /* 检查共享key,可能扩容哈希表
*/
if (_PyDict_HasSplitTable(mp) &&
((ix >= 0 && old_value == NULL && mp->ma_used != ix) ||
(ix == DKIX_EMPTY && mp->ma_used != mp->ma_keys->dk_nentries))) {
if (insertion_resize(mp) < 0)
goto Fail;
ix = DKIX_EMPTY;
}
//搜索成功
if (ix == DKIX_EMPTY) {
/* 插入一个新的slot,这个slot可以直接看成是entry */
assert(old_value == NULL);
if (mp->ma_keys->dk_usable <= 0) {
/* 需要resize */
if (insertion_resize(mp) < 0)
goto Fail;
}
//寻找值的插入位置,就是我们之前说的将key这个值通过哈希函数映射为索引
Py_ssize_t hashpos = find_empty_slot(mp->ma_keys, hash);
//拿到PyDictKeyEntry *指针
ep = &DK_ENTRIES(mp->ma_keys)[mp->ma_keys->dk_nentries];
//设置
dk_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries);
ep->me_key = key; //设置key
ep->me_hash = hash;//设置哈希
//如果ma_values数组不为空
if (mp->ma_values) {
assert (mp->ma_values[mp->ma_keys->dk_nentries] == NULL);
//设置进去,还记得这是什么表吗?对,这是一张split table
mp->ma_values[mp->ma_keys->dk_nentries] = value;
}
else {
//ma_values数据为空的话,那么value就设置在PyDictKeyEntry对象的me_value里面
ep->me_value = value;
} mp->ma_used++;//使用个数+1
mp->ma_version_tag = DICT_NEXT_VERSION();//版本数+1
mp->ma_keys->dk_usable--;//可用数-1
mp->ma_keys->dk_nentries++;//里面entry数量+1
assert(mp->ma_keys->dk_usable >= 0);
assert(_PyDict_CheckConsistency(mp));
return 0;
} //判断key是否存在,存在即替换
if (_PyDict_HasSplitTable(mp)) {
mp->ma_values[ix] = value;
if (old_value == NULL) {
/* pending state */
assert(ix == mp->ma_used);
mp->ma_used++;
}
}
else {
assert(old_value != NULL);
DK_ENTRIES(mp->ma_keys)[ix].me_value = value;
} mp->ma_version_tag = DICT_NEXT_VERSION();
Py_XDECREF(old_value); /* which **CAN** re-enter (see issue #22653) */
assert(_PyDict_CheckConsistency(mp));
Py_DECREF(key);
return 0; Fail:
Py_DECREF(value);
Py_DECREF(key);
return -1;
}

以上是插入元素,我们看到无论是插入元素、还是设置元素,insertdict都是可以胜任。但是请注意一下参数,有一个hash参数,这个hash是从什么地方获取的呢?答案是,在调用这个insertdict之前其实会首先调用PyDict_SetItem

int
PyDict_SetItem(PyObject *op, PyObject *key, PyObject *value)
{
PyDictObject *mp;
Py_hash_t hash;
if (!PyDict_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
assert(key);
assert(value);
mp = (PyDictObject *)op;
//计算hash值
if (!PyUnicode_CheckExact(key) ||
(hash = ((PyASCIIObject *) key)->hash) == -1)
{
//
hash = PyObject_Hash(key);
if (hash == -1)
return -1;
} /* 调用insertdict,必要时调整元素 */
return insertdict(mp, key, hash, value);
}

我们说如果entry个数达到容量的三分之二,那么会调整容量,如何调整呢?

//增长率
#define GROWTH_RATE(d) ((d)->ma_used*3) static int
insertion_resize(PyDictObject *mp)
{
//本质上调用了dictresize,传入PyDictObject * 和增长率
return dictresize(mp, GROWTH_RATE(mp));
} static int
dictresize(PyDictObject *mp, Py_ssize_t minsize)
{
//新的容量,entry的个数
Py_ssize_t newsize, numentries;
//老的keys
PyDictKeysObject *oldkeys;
//老的values
PyObject **oldvalues;
//老的entries,新的entries
PyDictKeyEntry *oldentries, *newentries; /* 确定table的大小*/
for (newsize = PyDict_MINSIZE;
newsize < minsize && newsize > 0;
newsize <<= 1)
;
if (newsize <= 0) {
PyErr_NoMemory();
return -1;
} //获取原来的所有keys
oldkeys = mp->ma_keys; /* 创建能够容纳newsize个entry的内存空间 */
mp->ma_keys = new_keys_object(newsize);
if (mp->ma_keys == NULL) {
//把以前的key拷贝过去。
/*
扩容并不是在本地扩容的,我们知道python存储的都是指针
当扩容之后,会在另一个地方申请更大的内存,然后会把之前的内容都拷贝过去
还是那句话,存储的是指针,不管拷贝到什么地方去,指针是不会变的,当然指针指向的值也是不会变的
但是指针的地址会变,因为指针也是一个变量,存储的是指针, 所以叫做指针变量
但不管咋样,总归是变量,自然也是有地址的,指针的指针就是我们所说的二级指针
可以承认的是, 拷贝之后,这些二级指针肯定会变。
然而在python中是体现不出来的,因为python里面没有二级指针的概念,甚至指针也没有。
你只能通过id查看内存地址,比如列表,虽然列表里面存储的本身就是地址,但是获取的时候确实个指针指向的值。
当然使用id查看地址,其实查看的就是列表里面的指针指向的值的地址,对,说白了就是列表里面的元素(指针)本身。
因此地址的地址你在python中是看不到的。
*/
mp->ma_keys = oldkeys;
return -1;
}
//必须满足 可用 >= 已用
assert(mp->ma_keys->dk_usable >= mp->ma_used);
if (oldkeys->dk_lookup == lookdict)
mp->ma_keys->dk_lookup = lookdict; //获取已用entries
numentries = mp->ma_used;
//获取旧信息
oldentries = DK_ENTRIES(oldkeys);
newentries = DK_ENTRIES(mp->ma_keys);
oldvalues = mp->ma_values;
//如果oldvalues不为NULL,这应该是一个combined table
//split table的特点是key是能是unicode、
//那么需要把split table转换成combined table
if (oldvalues != NULL) {
for (Py_ssize_t i = 0; i < numentries; i++) { assert(oldvalues[i] != NULL);
//将ma_values数组里面的元素统统都设置到PyDictKeyEntry对象里面去
PyDictKeyEntry *ep = &oldentries[i];
PyObject *key = ep->me_key;
Py_INCREF(key);
newentries[i].me_key = key;
newentries[i].me_hash = ep->me_hash;
newentries[i].me_value = oldvalues[i];
} //减少原来对oldkeys的引用计数
DK_DECREF(oldkeys);
//将ma_values设置为NULL,因为所有的value都存在了PyDictKeyEntry对象的me_value里面
mp->ma_values = NULL;
if (oldvalues != empty_values) {
free_values(oldvalues);
}
}
else { // 否则的话说明这本身就是一个combined table
if (oldkeys->dk_nentries == numentries) {
//将就得entries拷贝到新的entries里面去
memcpy(newentries, oldentries, numentries * sizeof(PyDictKeyEntry));
}
else {
//处理旧的entries
//active态的entry搬到新table中
//dummy态的entry,调整key的引用计数,丢弃该entry
PyDictKeyEntry *ep = oldentries;
for (Py_ssize_t i = 0; i < numentries; i++) {
while (ep->me_value == NULL)
ep++;
newentries[i] = *ep++;
}
} //字典缓冲池的操作,后面介绍
assert(oldkeys->dk_lookup != lookdict_split);
assert(oldkeys->dk_refcnt == 1);
if (oldkeys->dk_size == PyDict_MINSIZE &&
numfreekeys < PyDict_MAXFREELIST) {
DK_DEBUG_DECREF keys_free_list[numfreekeys++] = oldkeys;
}
else {
DK_DEBUG_DECREF PyObject_FREE(oldkeys);
}
} //建立哈希表索引
build_indices(mp->ma_keys, newentries, numentries);
mp->ma_keys->dk_usable -= numentries;
mp->ma_keys->dk_nentries = numentries;
return 0;
}

我们再来看一下改变dict内存空间的一些动作

  • 首先要确定table的大小,很显然这个大小一定要大于minsize,这个minsize通过我们已经看到了,是通过宏定义的,是已用entry的3倍
  • 根据新的table,重新申请内存
  • 将原来的处于active状态的entry拷贝到新的内存当中,而对于处于dummy状态的entry则直接丢弃。之所以可以丢弃,是因为,dummy状态的entry存在是为了保证探测链不断裂,但是现在所有的active都拷贝到新的内存当中了,它们会形成一条新的探测链,因此也就不需要这些dummy态的entry了
  • 建立的新的索引,并且如果之前的table指向了一片系统堆的内存空间,那么我们还需要释放,以防止内存泄漏。

6.3.5 删除元素

插入元素(设置元素)如果明白了,删除元素我觉得都可以不需要说了。

int
PyDict_DelItem(PyObject *op, PyObject *key)
{
//这显然和dictresize一样,是先获取hash值
Py_hash_t hash;
assert(key);
if (!PyUnicode_CheckExact(key) ||
(hash = ((PyASCIIObject *) key)->hash) == -1) {
hash = PyObject_Hash(key);
if (hash == -1)
return -1;
} //真正来删除是下面这个函数
return _PyDict_DelItem_KnownHash(op, key, hash);
} int
_PyDict_DelItem_KnownHash(PyObject *op, PyObject *key, Py_hash_t hash)
{
Py_ssize_t ix;
PyDictObject *mp;
PyObject *old_value; //类型检测
if (!PyDict_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
assert(key);
assert(hash != -1);
mp = (PyDictObject *)op;
//获取对应entry的index
ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &old_value);
if (ix == DKIX_ERROR)
return -1;
if (ix == DKIX_EMPTY || old_value == NULL) {
_PyErr_SetKeyError(key);
return -1;
} // split table不支持删除操作,如果是split table,需要转换成combined table
if (_PyDict_HasSplitTable(mp)) {
if (dictresize(mp, DK_SIZE(mp->ma_keys))) {
return -1;
}
ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &old_value);
assert(ix >= 0);
} //传入hash和ix,又调用了delitem_common
return delitem_common(mp, hash, ix, old_value);
} static int
delitem_common(PyDictObject *mp, Py_hash_t hash, Py_ssize_t ix,
PyObject *old_value)
{
PyObject *old_key;
PyDictKeyEntry *ep; //找到对应的hash索引
Py_ssize_t hashpos = lookdict_index(mp->ma_keys, hash, ix);
assert(hashpos >= 0); //已经entries个数-1
mp->ma_used--;
//版本-1
mp->ma_version_tag = DICT_NEXT_VERSION();
//拿到entry的指针
ep = &DK_ENTRIES(mp->ma_keys)[ix];
//将其设置为dummy状态
dk_set_index(mp->ma_keys, hashpos, DKIX_DUMMY);
ENSURE_ALLOWS_DELETIONS(mp);
old_key = ep->me_key;
//将其key、value都设置为NULL
ep->me_key = NULL;
ep->me_value = NULL;
//减少引用计数
Py_DECREF(old_key);
Py_DECREF(old_value); assert(_PyDict_CheckConsistency(mp));
return 0;
}

流程非常清晰,也很简单。先使用PyDict_DelItem计算hash值,再使用_PyDict_DelItem_KnownHash计算出索引,最后使用delitem_common获取相应的entry,删除维护的元素,并将entry从active态设置为dummy态,同时还会调整ma_used(已用entry)的数量

6.4 PyDictObject对象缓冲池

从介绍PyLongObject的小整数对象池的时候,我们就说过,不同的对象都有自己的缓冲池,比如list,当然dict也不例外。

#ifndef PyDict_MAXFREELIST
#define PyDict_MAXFREELIST 80
#endif
static PyDictObject *free_list[PyDict_MAXFREELIST];
static int numfree = 0;

PyDictObject的缓冲池机制其实和PyListObject的缓冲池是类似的,开始时,这个缓冲池什么也没有,直到第一个PyDictObject对象被销毁时,这个PyDictObject缓冲池里面才开始接纳被缓冲的PyDictObject对象。

static void
dict_dealloc(PyDictObject *mp)
{
//获取ma_values指针
PyObject **values = mp->ma_values;
//获取所有的ma_keys指针
PyDictKeysObject *keys = mp->ma_keys;
//两个整型
Py_ssize_t i, n; //追踪、调试
PyObject_GC_UnTrack(mp);
Py_TRASHCAN_SAFE_BEGIN(mp) //调整引用计数
if (values != NULL) {
if (values != empty_values) {
for (i = 0, n = mp->ma_keys->dk_nentries; i < n; i++) {
Py_XDECREF(values[i]);
}
free_values(values);
}
DK_DECREF(keys);
}
else if (keys != NULL) {
assert(keys->dk_refcnt == 1);
DK_DECREF(keys);
}
//将被销毁的对象放到缓冲池当中
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)
}

和PyListObject对象的缓冲池机制一样,缓冲池中只保留了PyDictObject对象。如果维护的维护的是从系统堆中申请的内存空间,那么python将释放这份内存空间,归还给系统堆。如果不是,那么仅仅只需要调整维护的对象的引用计数即可

其实在创建一个PyDictObject对象时,如果缓冲池中有可用的对象,也会直接从缓冲池中取,而不需要再重新创建。

static PyObject *
new_dict(PyDictKeysObject *keys, PyObject **values)
{
PyDictObject *mp;
assert(keys != NULL);
if (numfree) {
mp = free_list[--numfree];
assert (mp != NULL);
assert (Py_TYPE(mp) == &PyDict_Type);
_Py_NewReference((PyObject *)mp);
}
...
...
...
04-30 01:56