目标:
弄清楚python中dict的实现原理
1 dict原理
1.1 原理
python的字典实际是哈希表,通过哈希函数将key映射到表中的位置来
存储value。
存在不同对象经过哈希函数得到的哈希值可能相同,此时
采用开放定址法来解决冲突。
开放定址法通过二次探测函数f寻找下一个候选位置,若位置可用,
则将数据插入。
使用二次探测函数f从一个位置出发可以到达多个位置,这些位置形成了冲突探测链,
此时删除链路上某个元素,采用标记法来进行逻辑删除。
1.2 状态
python字典的3种状态
Unused: 字典还未存储键值对,初始化的字典都是该状态
Active: 字典存储键值对时
Dummy: 字典的键值对被删除时,将key的状态改为Dummy。
2 源码分析
主入口:
typedef struct {
PyObject_HEAD
/* Number of items in the dictionary */
Py_ssize_t ma_used;
/* Dictionary version: globally unique, value change each time
the dictionary is modified */
uint64_t ma_version_tag;
PyDictKeysObject *ma_keys;
/* If ma_values is NULL, the table is "combined": keys and values
are stored in ma_keys.
If ma_values is not NULL, the table is splitted:
keys are stored in ma_keys and values are stored in ma_values */
PyObject **ma_values;
} PyDictObject;
分析:
2.1) 字典是一个结构体,包含了:
ma_used,ma_keys,ma_values参数。
ma_used表示字典中键值对的个数,
ma_keys表示
ma_values如果不空,则keys存储在ma_keys,values存储在ma_values;否则
键值都存储在ma_keys中。
2.2) 创建字典
调用PyDict_New(void)
源码如下:
PyObject *
PyDict_New(void)
{
PyDictKeysObject *keys = new_keys_object(PyDict_MINSIZE);
if (keys == NULL)
return NULL;
return new_dict(keys, NULL);
}
分析:
new_keys_object进行容量检查和根据容量申请内存,具体参见2.2.1
new_dict创建字典,具体参见2.2.2
2.2.1) new_keys_object代码如下
static PyDictKeysObject *new_keys_object(Py_ssize_t size)
{
PyDictKeysObject *dk;
Py_ssize_t es, usable;
assert(size >= PyDict_MINSIZE);
assert(IS_POWER_OF_2(size));
usable = USABLE_FRACTION(size);
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);
}
if (size == PyDict_MINSIZE && numfreekeys > 0) {
dk = keys_free_list[--numfreekeys];
}
else {
dk = PyObject_MALLOC(sizeof(PyDictKeysObject)
- Py_MEMBER_SIZE(PyDictKeysObject, dk_indices)
+ es * size
+ sizeof(PyDictKeyEntry) * usable);
if (dk == NULL) {
PyErr_NoMemory();
return NULL;
}
}
DK_DEBUG_INCREF dk->dk_refcnt = 1;
dk->dk_size = size;
dk->dk_usable = usable;
dk->dk_lookup = lookdict_unicode_nodummy;
dk->dk_nentries = 0;
memset(&dk->dk_indices.as_1[0], 0xff, es * size);
memset(DK_ENTRIES(dk), 0, sizeof(PyDictKeyEntry) * usable);
return dk;
}
2.2.2) new_dict分析
源码如下:
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;
}
}
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;
}
分析:
new_dict方法优先从缓存中虎丘内存,如果没有缓存,则
调用PyObject_GC_New创建字典对象并初始化key和value
2.3 字典查找
分析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;
PyDictKeysObject *dk;
PyDictKeyEntry *ep0;
top:
dk = mp->ma_keys;
ep0 = DK_ENTRIES(dk);
mask = DK_MASK(dk);
perturb = hash;
i = (size_t)hash & mask;
for (;;) {
Py_ssize_t ix = dk_get_index(dk, i);
if (ix == DKIX_EMPTY) {
*value_addr = NULL;
return ix;
}
if (ix >= 0) {
PyDictKeyEntry *ep = &ep0[ix];
assert(ep->me_key != NULL);
if (ep->me_key == key) {
*value_addr = ep->me_value;
return ix;
}
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();
}
分析:
通过 i = (size_t)hash & mask; 来进行 进行定位探测冲突链。
dk_get_index 方法来搜索key对应的值,然后将查询的key和与字典中的key比较成功则返回数据
再比较两个key 之间的hash 是否相同如果相同使用PyObject_RichCompareBool方法比较,成功则返回数据。
如果key经过前面的比较都不相同,则在探测链上继续往下寻找。
3 总结
python字典本质上是哈希表,与C++中的map(本质是红黑树)还不一样。
通过开放定址法使用二次探测来解决哈希冲突。
字典有Unused,Active,Dummy三种状态。删除的键值对实际是通过
设置key为dummy来做伪删除。
查找key对应的value时,通过 i = (size_t)hash & mask; 来进行 进行定位探测冲突链。
dk_get_index 方法来搜索key对应的值,然后将查询的key和与字典中的key比较成功则返回数据
再比较两个key 之间的hash 是否相同如果相同使用PyObject_RichCompareBool方法比较,成功则返回数据。
如果key经过前面的比较都不相同,则在探测链上继续往下寻找。
参考:
https://blog.csdn.net/lucky404/article/details/79606089
https://github.com/python/cpython/blob/master/Objects/dictobject.c
https://github.com/python/cpython/blob/master/Include/dictobject.h
https://blog.csdn.net/qq_33339479/article/details/81835379