字典是怎麼創建的,支持的操作又是如何實現的?

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章