【通过Cpython3.9源码看看字典到底是咋回事】

字典结构

/* The ma_values pointer is NULL for a combined table
 * or points to an array of PyObject* for a split table
 */
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;

上述代码定义了 CPython 中的 PyDictObject 结构体,表示 Python 字典对象。

PyDictObject 结构体包含以下几个成员变量:

  1. PyObject_HEAD:Python 对象头部,包括对象类型、引用计数等信息。
  2. ma_used:字典中当前存储的键值对数量。
  3. ma_version_tag:字典的版本标记,每次字典被修改都会改变。
  4. ma_keys:指向 PyDictKeysObject 结构体的指针,表示字典中的键。
  5. ma_values:指向 PyObject* 类型的指针,表示字典中的值。

如果 ma_values 为 NULL,表示该字典采用“combined”方式存储,即键和值都存储在 ma_keys 中;如果 ma_values 不为 NULL,则采用“splitted”方式存储,即键存储在 ma_keys 中,值存储在 ma_values 中。

struct _dictkeysobject {
    Py_ssize_t dk_refcnt;

    /* Size of the hash table (dk_indices). It must be a power of 2. */
    Py_ssize_t dk_size;

    /* Function to lookup in the hash table (dk_indices):

       - lookdict(): general-purpose, and may return DKIX_ERROR if (and
         only if) a comparison raises an exception.

       - lookdict_unicode(): specialized to Unicode string keys, comparison of
         which can never raise an exception; that function can never return
         DKIX_ERROR.

       - lookdict_unicode_nodummy(): similar to lookdict_unicode() but further
         specialized for Unicode string keys that cannot be the  value.

       - lookdict_split(): Version of lookdict() for split tables. */
    dict_lookup_func dk_lookup;

    /* Number of usable entries in dk_entries. */
    Py_ssize_t dk_usable;

    /* Number of used entries in dk_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:

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

       Dynamically sized, SIZEOF_VOID_P is minimum. */
    char dk_indices[];  /* char is required to avoid strict aliasing. */

    /* "PyDictKeyEntry dk_entries[dk_usable];" array follows:
       see the DK_ENTRIES() macro */
};

上述代码定义了 CPython 中的 _dictkeysobject 结构体,表示 Python 字典对象中的键的数据结构。

_dictkeysobject 结构体包含以下几个成员变量:

  1. dk_refcnt:键对象的引用计数。
  2. dk_size:哈希表的大小,必须是 2 的幂次方。
  3. dk_lookup:指向查找哈希表中元素的函数指针。
  4. dk_usable:哈希表中可用的条目数。
  5. dk_nentries:哈希表中已经使用的条目数。
  6. dk_indices:哈希表,保存了索引,或者是 DKIX_EMPTY(-1),或者是 DKIX_DUMMY(-2)。索引值必须满足 0 <= indice < USABLE_FRACTION(dk_size),其中 USABLE_FRACTION(dk_size) 是可用的索引数量,即 dk_usable。索引的大小取决于 dk_size 的大小,当 dk_size <= 0xff 时,索引为 char* 类型,当 dk_size <= 0xffff 时,索引为 int16_t* 类型,当 dk_size <= 0xffffffff 时,索引为 int32_t* 类型,否则为 int64_t* 类型。
  7. dk_entries:保存键对象的数组,dk_usabledk_nentries 决定了数组的长度,具体的长度由 DK_ENTRIES() 宏计算得到。

总之,_dictkeysobject 结构体定义了 Python 字典对象中键的基本属性和结构。

字典元素查找

1280116143@qq.com
PyObject *
PyDict_GetItem(PyObject *op, PyObject *key)
{
    Py_hash_t hash;
    Py_ssize_t ix;
    PyDictObject *mp = (PyDictObject *)op;
    PyThreadState *tstate;
    PyObject *value;

    if (!PyDict_Check(op))
        return NULL;
    if (!PyUnicode_CheckExact(key) ||
        (hash = ((PyASCIIObject *) key)->hash) == -1)
    {
        hash = PyObject_Hash(key);
        if (hash == -1) {
            PyErr_Clear();
            return NULL;
        }
    }

    /* We can arrive here with a NULL tstate during initialization: try
       running "python -Wi" for an example related to string interning.
       Let's just hope that no exception occurs then...  This must be
       _PyThreadState_GET() and not PyThreadState_Get() because the latter
       abort Python if tstate is NULL. */
    tstate = _PyThreadState_GET();
    if (tstate != NULL && tstate->curexc_type != NULL) {
        /* preserve the existing exception */
        PyObject *err_type, *err_value, *err_tb;
        PyErr_Fetch(&err_type, &err_value, &err_tb);
        ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &value);
        /* ignore errors */
        PyErr_Restore(err_type, err_value, err_tb);
        if (ix < 0)
            return NULL;
    }
    else {
        ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &value);
        if (ix < 0) {
            PyErr_Clear();
            return NULL;
        }
    }
    return value;
}

PyDict_GetItem函数是用于从字典中获取一个键对应的值的函数。其函数定义如下:

PyObject *PyDict_GetItem(PyObject *op, PyObject *key)
  • op: 要从中获取键值对的字典对象。
  • key: 要获取值的键对象。

该函数首先会检查 op 是否为字典对象,如果不是则返回 NULL。接着会检查 key 是否为 Unicode 字符串对象,如果不是则需要对其进行哈希运算。如果哈希运算失败则会清除错误并返回 NULL

然后该函数调用字典对象的 dk_lookup 函数进行查找。如果查找失败则会根据情况清除错误并返回 NULL。如果查找成功则返回相应的值对象。dk_lookup_dictkeysobject 结构体中的一个函数指针,它指向一个用于在哈希表中查找元素的函数。这个函数会接收三个参数:指向哈希表的 _dictkeysobject 指针,要查找的元素 key,和元素 key 的哈希值 hash。如果找到了元素,则把元素的值存储在指向 value 的指针中,并返回该元素在哈希表中的索引;如果没找到元素,则返回 -1。在不同的情况下,dk_lookup 可以指向不同的查找函数,例如 lookdict()lookdict_unicode() 等。

需要注意的是,如果函数调用过程中发生了异常,该函数会尝试保留异常信息,查找完成后再恢复之前的异常。

字典新增元素

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;
    if (!PyUnicode_CheckExact(key) ||
        (hash = ((PyASCIIObject *) key)->hash) == -1)
    {
        hash = PyObject_Hash(key);
        if (hash == -1)
            return -1;
    }

    if (mp->ma_keys == Py_EMPTY_KEYS) {
        return insert_to_emptydict(mp, key, hash, value);
    }
    /* insertdict() handles any resizing that might be necessary */
    return insertdict(mp, key, hash, value);
}

PyDict_SetItem是向字典中设置一个键值对的函数,其参数包括一个指向待操作字典的指针 op,一个键 key 和一个值 value

首先,PyDict_SetItem检查 op 是否是一个字典,如果不是,则返回错误。

接着,key 被哈希并存储在变量 hash 中,如果 key 不是 Unicode 对象或者 hash 值为 -1,则调用 PyObject_Hashkey 进行哈希。如果 PyObject_Hash 失败,则返回错误。

然后,如果字典为空(即不存在键值对),则调用 insert_to_emptydict 将键值对添加到字典中。如果字典非空,则调用 insertdict 函数将键值对添加到字典中,并处理可能需要的重新哈希和重新分配内存等操作。最后,函数返回一个整数,表示操作是否成功。

// Same to insertdict but specialized for ma_keys = Py_EMPTY_KEYS.
static int
insert_to_emptydict(PyDictObject *mp, PyObject *key, Py_hash_t hash,
                    PyObject *value)
{
    assert(mp->ma_keys == Py_EMPTY_KEYS);

    PyDictKeysObject *newkeys = new_keys_object(PyDict_MINSIZE);
    if (newkeys == NULL) {
        return -1;
    }
    if (!PyUnicode_CheckExact(key)) {
        newkeys->dk_lookup = lookdict;
    }
    dictkeys_decref(Py_EMPTY_KEYS);
    mp->ma_keys = newkeys;
    mp->ma_values = NULL;

    Py_INCREF(key);
    Py_INCREF(value);
    MAINTAIN_TRACKING(mp, key, value);

    size_t hashpos = (size_t)hash & (PyDict_MINSIZE-1);
    PyDictKeyEntry *ep = DK_ENTRIES(mp->ma_keys);
    dictkeys_set_index(mp->ma_keys, hashpos, 0);
    ep->me_key = key;
    ep->me_hash = hash;
    ep->me_value = value;
    mp->ma_used++;
    mp->ma_version_tag = DICT_NEXT_VERSION();
    mp->ma_keys->dk_usable--;
    mp->ma_keys->dk_nentries++;
    return 0;
}

insert_to_emptydict 函数是用于将一个键值对添加到一个空的字典中的函数。它会创建一个新的 PyDictKeysObject 对象,指向的数组大小为 PyDict_MINSIZE,并将指定的键值对存储在该数组中的第一个位置。函数返回0表示成功,返回-1表示失败。在添加键值对之前,函数会增加键和值的引用计数,并在字典中记录这些引用,以便可以在之后跟踪这些对象。如果函数成功,它会更新字典的计数器和版本标记。如果出现任何错误,它会清理之前增加的引用计数并返回 -1。

/*
Internal routine to insert a new item into the table.
Used both by the internal resize routine and by the public insert routine.
Returns -1 if an error occurred, or 0 on success.
*/
static int
insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
{
    PyObject *old_value;
    PyDictKeyEntry *ep;

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

    /* When insertion order is different from shared key, we can't share
     * the key anymore.  Convert this instance to combine table.
     */
    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) {
        /* Insert into new slot. */
        assert(old_value == NULL);
        if (mp->ma_keys->dk_usable <= 0) {
            /* Need to resize. */
            if (insertion_resize(mp) < 0)
                goto Fail;
        }
        Py_ssize_t hashpos = find_empty_slot(mp->ma_keys, hash);
        ep = &DK_ENTRIES(mp->ma_keys)[mp->ma_keys->dk_nentries];
        dictkeys_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries);
        ep->me_key = key;
        ep->me_hash = hash;
        if (mp->ma_values) {
            assert (mp->ma_values[mp->ma_keys->dk_nentries] == NULL);
            mp->ma_values[mp->ma_keys->dk_nentries] = value;
        }
        else {
            ep->me_value = value;
        }
        mp->ma_used++;
        mp->ma_version_tag = DICT_NEXT_VERSION();
        mp->ma_keys->dk_usable--;
        mp->ma_keys->dk_nentries++;
        assert(mp->ma_keys->dk_usable >= 0);
        ASSERT_CONSISTENT(mp);
        return 0;
    }

    if (old_value != value) {
        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_CONSISTENT(mp);
    Py_DECREF(key);
    return 0;

Fail:
    Py_DECREF(value);
    Py_DECREF(key);
    return -1;
}

这是 Python 字典对象中用于插入新条目的内部函数 insertdict 的实现代码。它用于将新的键值对插入到字典中。该函数有三个参数:字典对象 mp,键 key 和值 value

在插入之前,我们需要确保 key 和 value 的引用计数都增加了,以防止它们被误释放。如果字典对象拥有拆分表,且键不是 Unicode 字符串,就需要进行插入大小调整,调用 insertion_resize 函数。这是因为拆分表的索引只是包含字符集合的子集,所以添加元素时可能会出现哈希冲突,导致拆分表无法正确解决冲突。

接下来,调用字典键查找函数 mp->ma_keys->dk_lookup 来查找键在字典中的位置,如果找到,则将新值赋给原位置的值。如果键在字典中没有找到,就将新键值对插入到字典中。如果字典对象没有拆分表,则将新键值对放入 me_value 字段中。如果字典对象拥有拆分表,则将值放入 ma_values 中相应的索引中。

如果没有找到空闲位置,则需要进行扩容。我们需要为新条目找到一个空闲的哈希槽,并将该槽的索引插入到 dk_indices 数组中。新条目的指针存储在键入表中的相应索引处,存储新值。

最后,我们需要在引用计数计数中减少旧值的引用计数,以防止内存泄漏,将键的引用计数减少,以使字典对象正确工作,并返回成功或失败的结果。

你可能感兴趣的:(cpython源码分析,算法,哈希算法,python)