字典结构
/* 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
结构体包含以下几个成员变量:
PyObject_HEAD
:Python 对象头部,包括对象类型、引用计数等信息。- ma_used:字典中当前存储的键值对数量。
- ma_version_tag:字典的版本标记,每次字典被修改都会改变。
- ma_keys:指向
PyDictKeysObject
结构体的指针,表示字典中的键。 - 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 <dummy> 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
结构体包含以下几个成员变量:
dk_refcnt
:键对象的引用计数。dk_size
:哈希表的大小,必须是 2 的幂次方。dk_lookup
:指向查找哈希表中元素的函数指针。dk_usable
:哈希表中可用的条目数。dk_nentries
:哈希表中已经使用的条目数。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*
类型。dk_entries
:保存键对象的数组,dk_usable
和dk_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_Hash
对 key
进行哈希。如果 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
数组中。新条目的指针存储在键入表中的相应索引处,存储新值。
最后,我们需要在引用计数计数中减少旧值的引用计数,以防止内存泄漏,将键的引用计数减少,以使字典对象正确工作,并返回成功或失败的结果。