PyDictObject 的创建
解释器内部会通过PyDict_New来创建一个新的dict对象。
PyObject * PyDict_New(void) { //new_keys_object表示创建PyDictKeysObject*对象 //里面传一个数值,表示哈希表的容量 //#define PyDict_MINSIZE 8,从宏定义我们能看出来为8 PyDictKeysObject *keys = new_keys_object(PyDict_MINSIZE); if (keys == NULL) return NULL; //这一步则是根据PyDictKeysObject *创建一个新字典 return new_dict(keys, NULL); }
所以整个过程分为两步,先创建PyDictKeysObject,然后再根据PyDictKeysObject创建PyDictObject。
因此核心逻辑就在new_keys_object和new_dict里面。
static PyDictKeysObject *new_keys_object(Py_ssize_t size) { PyDictKeysObject *dk; Py_ssize_t es, usable; //检测,size是否>=PyDict_MINSIZE assert(size >= PyDict_MINSIZE); assert(IS_POWER_OF_2(size)); usable = USABLE_FRACTION(size); //es:哈希表中的每个索引占多少字节 //因为长度不同,哈希索引数组的元素大小也不同 if (size <= 0xff) { //小于等于255,采用1字节存储 es = 1; } else if (size <= 0xffff) { //小于等于65535,采用2字节存储 es = 2; } #if SIZEOF_VOID_P > 4 else if (size <= 0xffffffff) { //否则采用4字节 es = 4; } #endif else { es = sizeof(Py_ssize_t); } //然后是创建PyDictKeysObject,这里会优先从缓存池中获取 //当然,PyDictObject也有自己的缓存池 //所以这两者都有缓存池,具体细节下一篇会详细说 if (size == PyDict_MINSIZE && numfreekeys > 0) { dk = keys_free_list[--numfreekeys]; } else { //否则malloc重新申请内存 //注意这里申请的内存由三部分组成 //1)PyDictKeysObject结构体本身的大小 //2)哈希索引数组的长度乘以每个元素的大小、也就是es*size //3)键值对数组的长度乘上每个entry的大小 dk = PyObject_MALLOC(sizeof(PyDictKeysObject) + es * size + sizeof(PyDictKeyEntry) * usable); if (dk == NULL) { PyErr_NoMemory(); return NULL; } } //设置引用计数、可用的entry个数等信息 DK_DEBUG_INCREF dk->dk_refcnt = 1; dk->dk_size = size; dk->dk_usable = usable; //dk_lookup很关键,它表示探测函数 dk->dk_lookup = lookdict_unicode_nodummy; dk->dk_nentries = 0; //哈希表的初始化 memset(&dk->dk_indices[0], 0xff, es * size); memset(DK_ENTRIES(dk), 0, sizeof(PyDictKeyEntry) * usable); return dk; }
以上就是PyDictKeysObject的初始化过程,然后会再基于它创建PyDictObject,通过函数new_dict实现。
static PyObject * new_dict(PyDictKeysObject *keys, PyObject **values) { PyDictObject *mp; assert(keys != NULL); //PyDictObject的缓存池,具体细节下一篇说 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; } } //设置key、value等等 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; }
以上就是字典的创建,过程应该不算复杂。下面我们再来看看,字典支持的操作是如何实现的。
给字典添加键值对
我们通过d["name"] = "satori"即可给字典添加一个键值对,如果键存在则修改value。
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; //如果key不是字符串 //或者哈希值还没有计算的话 if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { //计算哈希值,PyObject_Hash是一个泛型API //会调用类型对象的tp_hash函数,因此等价于 //Py_TYPE(key) -> tp_hash(key) hash = PyObject_Hash(key); if (hash == -1) return -1; } /* 调用insertdict,必要时调整元素 */ return insertdict(mp, key, hash, value); }
所以这一步相当于计算函数的哈希值,真正的设置键值对逻辑藏在insertdict里面,我们来看一下。
static int insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value) { //key对应的value PyObject *old_value; //entry PyDictKeyEntry *ep; //增加对key和value的引用计数 Py_INCREF(key); Py_INCREF(value); //类型检查 if (mp->ma_values != NULL && !PyUnicode_CheckExact(key)) { if (insertion_resize(mp) < 0) goto Fail; } //mp->ma_keys->dk_lookup表示获取探测函数 //会基于传入的哈希值、key、判断哈希索引数组是否有可用的槽 Py_ssize_t ix = mp->ma_keys->dk_lookup(mp, key, hash, &old_value); if (ix == DKIX_ERROR) //不存在,跳转至Fail goto Fail; assert(PyUnicode_CheckExact(key) || mp->ma_keys->dk_lookup == lookdict); MAINTAIN_TRACKING(mp, key, value); //... if (ix == DKIX_EMPTY) { //如果ix==DKIX_EMPTY //说明哈希索引数组存在一个可用的槽 assert(old_value == NULL); if (mp->ma_keys->dk_usable <= 0) { /* 判断是否需要resize */ if (insertion_resize(mp) < 0) goto Fail; } //存在可用的槽,调用find_empty_slot //将可用槽的索引找到、并返回 Py_ssize_t hashpos = find_empty_slot(mp->ma_keys, hash); //拿到PyDictKeyEntry *指针 ep = &DK_ENTRIES(mp->ma_keys)[mp->ma_keys->dk_nentries]; //将该entry在键值对数组中的索引存储在指定的槽里面 dk_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries); ep->me_key = key; //设置key ep->me_hash = hash;//设置哈希 //但value还没有设置,因为还要判断哈希表的种类 //如果ma_values数组不为空,说明是分离表 //ma_keys只维护键 if (mp->ma_values) { assert (mp->ma_values[mp->ma_keys->dk_nentries] == NULL); //要将value保存在ma_values中 mp->ma_values[mp->ma_keys->dk_nentries] = value; } else { //否则是结合表 //那么value就设置在PyDictKeyEntry对象的me_value里面 ep->me_value = value; } mp->ma_used++;//使用个数+1 mp->ma_version_tag = DICT_NEXT_VERSION();//版本数+1 mp->ma_keys->dk_usable--;//可用数-1 mp->ma_keys->dk_nentries++;//里面entry数量+1 assert(mp->ma_keys->dk_usable >= 0); assert(_PyDict_CheckConsistency(mp)); return 0; } //走到这里说明key已经存在了,那么此时相当于修改 //将旧的value替换掉 if (_PyDict_HasSplitTable(mp)) { //分离表,修改ma_values mp->ma_values[ix] = value; if (old_value == NULL) { /* pending state */ assert(ix == mp->ma_used); mp->ma_used++; } } //结合表 //修改ma_keys->dk_entries中指定entry的me_value 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); assert(_PyDict_CheckConsistency(mp)); Py_DECREF(key); return 0; Fail: Py_DECREF(value); Py_DECREF(key); return -1; }
以上就是设置元素相关的逻辑,还是有点难度的,需要对着源码仔细理解一下。
根据key获取value
获取某个键对应的值,会执行PyDict_GetItem函数,但是核心逻辑是在dict_subscript函数里面,我们来看一下。
static PyObject * dict_subscript(PyDictObject *mp, PyObject *key) { Py_ssize_t ix; Py_hash_t hash; PyObject *value; //获取哈希值 if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); if (hash == -1) return NULL; } //是否存在可用的槽 //注意value传了一个指针进去 //所以当entry存在时,会将 value 设置为指定的值 ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &value); if (ix == DKIX_ERROR) return NULL; //注意这里是获取元素,如果key被映射到了该槽 //然后该槽还可用,这意味着什么呢?显然是不存在此key if (ix == DKIX_EMPTY || value == NULL) { if (!PyDict_CheckExact(mp)) { //如果其类型对象继承dict,那么在找不到key时 //会执行__missing__方法 PyObject *missing, *res; _Py_IDENTIFIER(__missing__); missing = _PyObject_LookupSpecial((PyObject *)mp, &PyId___missing__); //执行__missing__方法 if (missing != NULL) { res = PyObject_CallFunctionObjArgs(missing, key, NULL); Py_DECREF(missing); return res; } else if (PyErr_Occurred()) return NULL; } //报错,KeyError _PyErr_SetKeyError(key); return NULL; } //否则就说明value获取到了 //增加引用计数,返回value Py_INCREF(value); return value; }
逻辑比较简单,重点是里面出现了__missing__方法,这个方法只有写在继承dict的类里面才有用,我们举个栗子:
class MyDict(dict): def __getitem__(self, item): # 执行 MyDict()["xx"] # 会走这里的魔法函数 print("__getitem__") # 然后调用父类的__getitem__ # 父类在执行__getitem__时发现key不存在 # 会调用__missing__方法,并且会将key作为参数 return super().__getitem__(item + " satori") def __missing__(self, key): print(key) return key.upper() v = MyDict()["komeiji"] """ __getitem__ komeiji satori """ print(v) # KOMEIJI SATORI
删除某个键值对
设置键值对如果明白了,删除键值对我觉得都不需要说了。还是根据key找到指定的槽,如果槽里面的索引是DKIX_EMPTY,那么说明根本不存在此key,KeyError;否则拿到指定的entry,将其设置为dummy。
因为删除元素不能真正的删除,所以它本质还是有点类似于修改一个键值对。
int PyDict_DelItem(PyObject *op, PyObject *key) { //先获取hash值 Py_hash_t hash; assert(key); if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { hash = PyObject_Hash(key); if (hash == -1) return -1; } //真正来删除是下面这个函数 return _PyDict_DelItem_KnownHash(op, key, hash); } int _PyDict_DelItem_KnownHash(PyObject *op, PyObject *key, Py_hash_t hash) { //...... mp = (PyDictObject *)op; //获取对应entry的index ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &old_value); if (ix == DKIX_ERROR) return -1; if (ix == DKIX_EMPTY || old_value == NULL) { _PyErr_SetKeyError(key); return -1; } //...... //传入hash和ix,又调用了delitem_common return delitem_common(mp, hash, ix, old_value); } static int delitem_common(PyDictObject *mp, Py_hash_t hash, Py_ssize_t ix, PyObject *old_value) { PyObject *old_key; PyDictKeyEntry *ep; //找到指定的槽,拿到里面存储的索引 Py_ssize_t hashpos = lookdict_index(mp->ma_keys, hash, ix); assert(hashpos >= 0); //已用的entries个数-1 mp->ma_used--; //版本号增加 mp->ma_version_tag = DICT_NEXT_VERSION(); //拿到entry的指针 ep = &DK_ENTRIES(mp->ma_keys)[ix]; //先将dk_entries数组中指定的entry设置为dummy状态 dk_set_index(mp->ma_keys, hashpos, DKIX_DUMMY); ENSURE_ALLOWS_DELETIONS(mp); old_key = ep->me_key; //将其key、value都设置为NULL ep->me_key = NULL; ep->me_value = NULL; //减少引用计数 Py_DECREF(old_key); Py_DECREF(old_value); assert(_PyDict_CheckConsistency(mp)); return 0; }
流程非常清晰,也很简单。先计算hash值,再计算出索引,最后获取相应的entry,将me_key、me_value设置为NULL,并减少指向对象的引用计数。同时将entry从active态设置为dummy态,并调整ma_used(已存在键值对)的数量。
以上就是本次分享的所有内容,想要了解更多欢迎前往公众号:Python编程学习圈,每日干货分享