python dict 源码解析

python字典源码(https://github.com/python/cpython/blob/master/Objects/dictobject.c, https://github.com/python/cpython/blob/master/Include/dictobject.h)

哈希表和哈希冲突概念

python的字典是一种哈希表,是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数(哈希函数),存放记录的数组叫做散列表(哈希表/hash table)。

在理想的状态下,不同的对象经过哈希函数计算出来的哈希值是不一样的,但是随着存储数据的增多,不同的对象经过哈希函数计算出的哈希值可能是一样的,这种情况就是哈希冲突

python 解决哈希冲突的方案 开放定址法(open addressing)

python 采用的是开放定址法(open addressing)来解决哈希冲突,其原理是产生哈希冲突时, python 会通过一个二次探测函数 f, 计算下一个候选位置,当下一个位置可用,则将数据插入该位置,如果不可用则再次调用探测函数 f,获得下一个候选位置,因此经过不断探测,总会找到一个可用的位置

开放定址法存在的问题

通过多次使用二次探测函数f,从一个位置出发就可以依次到达多个位置,我们认为这些位置形成了一个 ‘冲突探测链’ 当需要删除探测链上的某个数据时问题就产生了, 假如这条链路上的首个元素是 a 最后的元素是 c 现在需要删除 处于中间位置的 b ,这样就会导致探测链断裂, 当下一次搜索 c 时会从 a 出发 沿着链路一步步出发,但是中途的链路断了导致无法到达 c 的位置, 因此无法搜索到c
所以在采用 开放定地法解决哈希冲突的策略,删除链路上的某个元素时,不能真正的删除元素,只能进行 ‘伪删除’

python字典的 三种状态 Unused, Active, Dummy

1. Unused.  index == DKIX_EMPTY
   Does not hold an active (key, value) pair now and never did.  Unused can
   transition to Active upon key insertion.  This is each slot's initial state.

2. Active.  index >= 0, me_key != NULL and me_value != NULL
   Holds an active (key, value) pair.  Active can transition to Dummy or
   Pending upon key deletion (for combined and split tables respectively).
   This is the only case in which me_value != NULL.

3. Dummy.  index == DKIX_DUMMY  (combined only)
   Previously held an active (key, value) pair, but that was deleted and an
   active pair has not yet overwritten the slot.  Dummy can transition to
   Active upon key insertion.  Dummy slots cannot be made Unused again
   else the probe sequence in case of collision would have no way to know
   they were once active.

Unused 状态下也就是当该字典中还没有存储key 和 value 每个字典初始化时都是该状态
Active 当字典中存储了key 和 value 时状态就进入到了 Active
Dummy 当字典中的 key 和 value 被删除后字典不能从Active 直接进入 Unused 状态 否则就会出现之前提到的 冲突链路中断,实际上python进行删除字典元素时,会将key的状态改为Dummy ,这就是 python的 ‘伪删除技术’

python dict 源码解析_第1张图片

python 源码定义的字典

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;

创建字典

通过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 方法 主要是做容量检查以便根据容量申请内存

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

然后通过 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;
}

该方法用于创建字典
主要做了以下工作

  • 检查缓冲池是否有缓冲如果有缓冲则不需要再申请内存,之前从缓存池返回数据

  • 当没有缓冲时使用 PyObject_GC_New 创建字典对象

  • 初始化key 和 value

字典搜索元素, 根据 key 搜索元素

python 字典搜索元素 是依靠 lookdict来实现的 但是对于不同的情况 python 又提供了,很多其他版本的搜索方法 比如
lookdict_unicode (Specialized version for string-only keys),
lookdict_unicode_nodummy (Faster version of lookdict_unicode when it is known that no ‘dummy’ keys),
lookdict_split (
* Version of lookdict for split tables.
* All split tables and only split tables use this lookup function.
* Split tables only contain unicode keys and no dummy keys,
* so algorithm is the same as lookdict_unicode_nodummy.)

这里我们只考虑 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();
}

发表于 <2018-03-18 23:24> 未完待续

2018-3-20 续

1 字典底层通过 i = (size_t)hash & mask; 来进行 进行定位探测冲突链
2 使用 dk_get_index 方法来搜索key对应的值,如果搜索不到key 即 DKIX_EMPTY
则直接返回null

3 然后将查询的key 和 与字典中的key 比较 成功则返回数据
if (ep->me_key == key) {
*value_addr = ep->me_value;
return ix;
}

4 再比较两个key 之间的hash 是否相同 如果相同 使用 PyObject_RichCompareBool 方法 比较 成功则返回数据 1 失败则是 0 错误则是 -1 因此 当 PyObject_RichCompareBool 方法返回1 则说明去到了数据

5 如果key 经过前面的比较都不相同,则在探测链上继续往下寻找
perturb >>= PERTURB_SHIFT;
i = (i*5 + perturb + 1) & mask;

你可能感兴趣的:(python)