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編程學習圈,每日干貨分享