字典类型是Python中最常用的数据类型之一,它是一个键值对的集合,字典通过键来索引,关联到相对的值,理论上它的查询复杂度是 O(1) :
- d = {'a': 1, 'b': 2} d['c'] = 3 d {'a': 1, 'b': 2, 'c': 3}
在字符串的实现原理文章中,曾经出现过字典对象用于intern操作,那么字典的内部结构是怎样的呢?PyDictObject 对象就是 dict 的内部实现。
哈希表(也叫散列表),根据关键值对(Key-value)而直接进行访问的数据结构。它通过把key和value映射到表中一个位置来访问记录,这种查询速度非常快,更新也快。而这个映射函数叫做哈希函数,存放值的数组叫做哈希表。 哈希函数的实现方式决定了哈希表的搜索效率。具体操作过程是:
但是,对key进行hash的时候,不同的key可能hash出来的结果是一样的,尤其是数据量增多的时候,这个问题叫做哈希冲突。如果解决这种冲突情况呢?通常的做法有两种,一种是链接法,另一种是开放寻址法,Python选择后者。
开放寻址法中,所有的元素都存放在散列表里,当产生哈希冲突时,通过一个探测函数计算出下一个候选位置,如果下一个获选位置还是有冲突,那么不断通过探测函数往下找,直到找个一个空槽来存放待插入元素。
字典中的一个key-value键值对元素称为entry(也叫做slots),对应到Python内部是PyDictEntry,PyDictObject就是PyDictEntry的集合。PyDictEntry的定义是:
- typedef struct {
- /* Cached hash code of me_key. Note that hash codes are C longs.
- * We have to use Py_ssize_t instead because dict_popitem() abuses
- * me_hash to hold a search finger.
- */
- Py_ssize_t me_hash;
- PyObject *me_key;
- PyObject *me_value;
- } PyDictEntry;
-
me_hash用于缓存me_key的哈希值,防止每次查询时都要计算哈希值,entry有三种状态。
为什么 entry 有 Dummy 状态呢?这是因为采用开放寻址法中,遇到哈希冲突时会找到下一个合适的位置,例如某元素经过哈希计算应该插入到A处,但是此时A处有元素的,通过探测函数计算得到下一个位置 B,仍然有元素,直到找到位置 C 为止,此时ABC构成了探测链,查找元素时如果hash值相同,那么也是顺着这条探测链不断往后找,当删除探测链中的某个元素时,比如B,如果直接把B从哈希表中移除,即变成Unused状态,那么C就不可能再找到了,因为AC之间出现了断裂的现象,正是如此才出现了第三种状态---Dummy,Dummy是一种类似的伪删除方式,保证探测链的连续性。
PyDictObject 就是 PyDictEntry 对象的集合,PyDictObject 的结构是:
- typedef struct _dictobject PyDictObject;
- struct _dictobject {
- PyObject_HEAD
- Py_ssize_t ma_fill; /* # Active + # Dummy */
- Py_ssize_t ma_used; /* # Active */
-
- /* The table contains ma_mask + 1 slots, and that's a power of 2.
- * We store the mask instead of the size because the mask is more
- * frequently needed.
- */
- Py_ssize_t ma_mask;
-
- /* ma_table points to ma_smalltable for small tables, else to
- * additional malloc'ed memory. ma_table is never NULL! This rule
- * saves repeated runtime null-tests in the workhorse getitem and
- * setitem calls.
- */
- PyDictEntry *ma_table;
- PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
- PyDictEntry ma_smalltable[PyDict_MINSIZE];
- };
-
PyDictObject 使用 PyObject_HEAD 而不是 PyObject_Var_HEAD,虽然字典也是变长对象,但此处并不是通过 ob_size 来存储字典中元素的长度,而是通过ma_used字段。
- PyObject *
- PyDict_New(void)
- {
- register PyDictObject *mp;
- if (dummy == NULL) { /* Auto-initialize dummy */
- dummy = PyString_FromString("<dummy key>");
- if (dummy == NULL)
- return NULL;
- }
- if (numfree) {
- mp = free_list[--numfree];
- assert (mp != NULL);
- assert (Py_TYPE(mp) == &PyDict_Type);
- _Py_NewReference((PyObject *)mp);
- if (mp->ma_fill) {
- EMPTY_TO_MINSIZE(mp);
- } else {
- /* At least set ma_table and ma_mask; these are wrong
- if an empty but presized dict is added to freelist */
- INIT_NONZERO_DICT_SLOTS(mp);
- }
- assert (mp->ma_used == 0);
- assert (mp->ma_table == mp->ma_smalltable);
- assert (mp->ma_mask == PyDict_MINSIZE - 1);
- } else {
- mp = PyObject_GC_New(PyDictObject, &PyDict_Type);
- if (mp == NULL)
- return NULL;
- EMPTY_TO_MINSIZE(mp);
- }
- mp->ma_lookup = lookdict_string;
- return (PyObject *)mp;
- }
-
- static PyDictEntry *
- lookdict(PyDictObject *mp, PyObject *key, register long hash)
- {
- register size_t i;
- register size_t perturb;
- register PyDictEntry *freeslot;
- register size_t mask = (size_t)mp->ma_mask;
- PyDictEntry *ep0 = mp->ma_table;
- register PyDictEntry *ep;
- register int cmp;
- PyObject *startkey;
-
- i = (size_t)hash & mask;
- ep = &ep0[i];
- if (ep->me_key == NULL || ep->me_key == key)
- return ep;
-
- if (ep->me_key == dummy)
- freeslot = ep;
- else {
- if (ep->me_hash == hash) {
- startkey = ep->me_key;
- Py_INCREF(startkey);
- cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
- Py_DECREF(startkey);
- if (cmp < 0)
- return NULL;
- if (ep0 == mp->ma_table && ep->me_key == startkey) {
- if (cmp > 0)
- return ep;
- }
- else {
- /* The compare did major nasty stuff to the
- * dict: start over.
- * XXX A clever adversary could prevent this
- * XXX from terminating.
- */
- return lookdict(mp, key, hash);
- }
- }
- freeslot = NULL;
- }
-
- /* In the loop, me_key == dummy is by far (factor of 100s) the
- least likely outcome, so test for that last. */
- for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {
- i = (i << 2) + i + perturb + 1;
- ep = &ep0[i & mask];
- if (ep->me_key == NULL)
- return freeslot == NULL ? ep : freeslot;
- if (ep->me_key == key)
- return ep;
- if (ep->me_hash == hash && ep->me_key != dummy) {
- startkey = ep->me_key;
- Py_INCREF(startkey);
- cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
- Py_DECREF(startkey);
- if (cmp < 0)
- return NULL;
- if (ep0 == mp->ma_table && ep->me_key == startkey) {
- if (cmp > 0)
- return ep;
- }
- else {
- /* The compare did major nasty stuff to the
- * dict: start over.
- * XXX A clever adversary could prevent this
- * XXX from terminating.
- */
- return lookdict(mp, key, hash);
- }
- }
- else if (ep->me_key == dummy && freeslot == NULL)
- freeslot = ep;
- }
- assert(0); /* NOT REACHED */
- return 0;
- }
-
字典在添加元素和查询元素时,都需要用到字典的搜索策略,搜索时,如果不存在该key,那么返回Unused状态的entry,如果存在该key,但是key是一个Dummy对象,那么返回Dummy状态的entry,其他情况就表示存在Active状态的entry,那么对于字典的插入操作,针对不同的情况进行操作也不一样。对于Active的entry,直接替换me_value值即可;对于Unused或Dummy的entry,需要同时设置me_key,me_hash和me_value
PyDictObject对象缓冲池和PyListObject对象缓冲池的原理是类似的,都是在对象被销毁的时候把该对象添加到缓冲池中去,而且值保留PyDictObject对象本身,如果ma_table维护的时从系统堆中申请的空间,那么Python会释放这块内存,如果ma_table维护的是ma_smalltable,那么只需把smalltable中的元素的引用计数减少即可。
- static void
- dict_dealloc(register PyDictObject *mp)
- {
- register PyDictEntry *ep;
- Py_ssize_t fill = mp->ma_fill;
- PyObject_GC_UnTrack(mp);
- Py_TRASHCAN_SAFE_BEGIN(mp)
- for (ep = mp->ma_table; fill > 0; ep++) {
- if (ep->me_key) {
- --fill;
- Py_DECREF(ep->me_key);
- Py_XDECREF(ep->me_value);
- }
- }
- if (mp->ma_table != mp->ma_smalltable)
- PyMem_DEL(mp->ma_table);
- if (numfree < PyDict_MAXFREELIST && Py_TYPE(mp) == &PyDict_Type)
- free_list[numfree++] = mp;
- else
- Py_TYPE(mp)->tp_free((PyObject *)mp);
- Py_TRASHCAN_SAFE_END(mp)
- }