python 64式: 第49式、dict源码分析

目标:
弄清楚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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值