CPython對象模型:Dict

此係列前幾篇:html

CPython對象模型:基礎 python

CPython對象模型:整型算法

CPython對象模型:string數據結構

CPython對象模型:Listide

除了list之外,dict也是python中十分經常使用的一種基本數據結構。 並且,dict在python內部被大量應用, dict的效率會直接影響python的運行效率, 所以python的做者們對dict進行了精心的設計和優化。 本篇博客會從源碼出發仔細分析一下python中的dict。函數

1 結構

因爲對dict的效率有着嚴格要求, python中的dict採用了hash表來實現。 衆所周知,hash表可能存在有衝突, 解決衝突也有若干種方法, 典型的有開鏈法(即把衝突的元素排成一個鏈,它們放在同一個格子下) 和開放地址法(即給衝突元素找一個新的地址,獨佔一個新的格子)。 python採用了開放地址法。oop

dict中用到了如下幾個數據結構:優化

  • PyDictObject:對應python中的dict
  • PyDictKeysObject:對應dict中全部key的集合
  • PyDictKeyEntry:對應一個(key, value)對

1.1 PyDictObject

/* file:Include/dictobject.h */
typedef struct {
    PyObject_HEAD
    Py_ssize_t ma_used;
    PyDictKeysObject *ma_keys;
    PyObject **ma_values;
} PyDictObject;
  • ma_used:表明了當前dict中已用存儲單元的個數
  • ma_keys:dict中key的集合(也可能會有值)
  • ma_values:可能存放dict中的value,也可能什麼也沒有。

這裏須要說一下dict的特殊之處。this

dict中把一個(key, value)對稱爲一個slot(嚴格來講一個slot是指一個PyDictEntry類型的變量)。 python中的slot存在4種不一樣的狀態:spa

  • Unused
  • Active
  • Dummy
  • Pending

Unused表示此slot還沒有使用,全部slot都會初始化成該狀態; Active表示此slot正在使用中; Dummy表示此slot已被刪除; Pending表示此slot還沒有被插入到dict中或還沒有被刪除。

爲何會存在表示已刪除的狀態呢? 以前提到過,dict採用了開放地址法, 存在衝突時會依次尋找新的地址,直到找到空的slot(Unused狀態) 或者須要查找的key。這樣的查找過程,實際上造成了一個查找鏈, 查找鏈的結尾是一個Unused或Active狀態的slot。 發現問題了吧?若是查找鏈中間某一個slot被刪除, 並從新回到了Unused狀態,那麼這個查找鏈就會被截斷, 致使後面的部分不能被找到。 所以Dummy存在的意義就在於維持查找鏈的完整。

爲何看起來像存儲容器的ma_values可能啥也不存呢? 由於python中的dict有兩種,一種叫combined-table dict, 一種叫split-table dict。 combined-talbe dict會把值以(key, value)的形式存儲在ma_keys中, 而split-table dict纔會把值存放在ma_values中。

這兩種dict有什麼區別呢? 用途上的區別暫且不提,內容上的區別 除了以前提到的值存儲位置的不一樣外, 還有就是split-table dict中全部的key必須爲string類型, 且不能存在dummy狀態的key。 combined-talbe dict中key能夠爲任意類型的對象, 可是slot不能出現pending狀態。

1.2 PyDictKeysObject

/* file:Objects/dictobject.c */
struct _dictkeysobject {
    Py_ssize_t dk_refcnt;
    Py_ssize_t dk_size;
    dict_lookup_func dk_lookup;
    Py_ssize_t dk_usable;
    PyDictKeyEntry dk_entries[1];
};
typedef _dictkeysobject PyDictKeysObject
  • dk_refcnt:引用數。PyDictKeysObject不是衍生自PyObject,因此須要額外加上這個
  • dk_size:hash表大小
  • dk_lookup:查找函數。python針對不一樣類型的dict作了若干查找優化,因此dict的查找函數可能不一樣
  • dk_usable:dict的剩餘可用大小(對於空dict,dk_usable = (2*dk_size+1)/3)。 研究發現,hash表中填充率超過2/3後衝突率會急劇上升, 所以爲了提升效率,python設定dict中使用dk_usable個slot時(即dk_usable ≤ 0時)會觸發resize擴大dict以保持低衝突率。 dk_usable會減小當且僅當unused狀態的slot數量減小。
  • dk_entries:實際的數據區。和以前的變長對象相似, 分配空間時都會在dk_entries後分配多餘空間以便使用。

1.3 PyDictKeyEntry

/* file:Objects/dictobject.c */
typedef struct {
    /* Cached hash code of me_key. */
    Py_hash_t me_hash;
    PyObject *me_key;
    PyObject *me_value; /* This field is only meaningful for combined tables */
} PyDictKeyEntry;

 

  • me_hash:此key的hash值。儲存hash值能夠避免重複計算,提升效率
  • me_value:若爲split table,me_value無心義。

前面說過,slot有四種狀態。 這四種狀態並未單獨保存,而是由me_key和me_value決定:

  • Unused
    • me_key == NULL
    • me_value == NULL
  • Active
    • me_key != NULL
    • me_key != dummy
    • me_value != NULL
  • Dummy
    • me_key == dummy
    • me_value == NULL
  • Pending
    • me_key != NULL
    • me_key !=dummy
    • me_value == NULL

這裏頻頻出現的dummy究竟是何方神聖? dummy的定義以下:

 
    
/* file:Objects/dictobject.c */
static PyObject _dummy_struct;
#define dummy (&_dummy_struct)
 
    
 
   

dummy就是一個獨一無二的PyObject而已。

2 dict的建立

 

2.1 建立dict

 
    
/* file:Objects/dictobject.c */
PyObject *
PyDict_New(void)
{
    PyDictKeysObject *keys = new_keys_object(PyDict_MINSIZE_COMBINED);
    if (keys == NULL)
        return NULL;
    return new_dict(keys, NULL);
}
 
    
 
   

可使用PyDict_New函數來新建一個dict。 這個函數的定義十分簡單,先新建一個大小爲PyDict_MINSIZE_COMBINED的keys, 而後新建dict便可。

這裏咱們會發現PyDict_MINSIZE_COMBINED這個常量。 這個常量是combined table的默認最小大小,它的值爲8。

須要注意,經過該函數會新建一個combine-table dict。 split-table dict更多的應用於python內部, 咱們使用的dict廣泛是combined-table dict。

2.1.1 新建keys

 
     
/* file:Objects/dictobject.c */
static PyDictKeysObject *new_keys_object(Py_ssize_t size)
{
    PyDictKeysObject *dk;
    Py_ssize_t i;
    PyDictKeyEntry *ep0;

    assert(size >= PyDict_MINSIZE_SPLIT);
    assert(IS_POWER_OF_2(size));
    dk = PyMem_MALLOC(sizeof(PyDictKeysObject) +
                      sizeof(PyDictKeyEntry) * (size-1));
    if (dk == NULL) {
        PyErr_NoMemory();
        return NULL;
    }
    DK_DEBUG_INCREF dk->dk_refcnt = 1;
    dk->dk_size = size;
    dk->dk_usable = USABLE_FRACTION(size);
    ep0 = &dk->dk_entries[0];
    /* Hash value of slot 0 is used by popitem, so it must be initialized */
    ep0->me_hash = 0;
    for (i = 0; i < size; i++) {
        ep0[i].me_key = NULL;
        ep0[i].me_value = NULL;
    }
    dk->dk_lookup = lookdict_unicode_nodummy;
    return dk;
}
 
     
 
    

這個函數的做用很簡單,就是根據給定的大小分配足夠的空間並初始化而已。 這裏值得注意的是函數開始的兩句斷言:

  • size >= PyDict_MINSIZE_SPLIT
  • IS_POWER_OF_2(size)

也就是說,keys最小大小爲PyDict_MINSIZE_SPLIT,且這個大小必須是2的n次方。

2.1.2 新建dict

 
     
/* file:Objects/dictobject.c */
static PyObject *
new_dict(PyDictKeysObject *keys, PyObject **values)
{
    PyDictObject *mp;
    assert(keys != NULL);
    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;
        }
    }
    mp->ma_keys = keys;
    mp->ma_values = values;
    mp->ma_used = 0;
    return (PyObject *)mp;
}
 
     
 
    

實際用來生成新的dict對象的函數爲new_dict。 在new_dict的實現中,又看到了和list中同樣的對象池機制。

實際上,該對象池的實現和list的幾乎如出一轍, 填充對象池都是在dealloc操做中完成。 相似的,dict對象池中的對象不會保留具體的數據區(ma_keys和ma_values)。

2.2 Key-sharing Dict

查看dictobject.c文件,會發現除了new_dict系列, 還有new_dict_with_shared_keys系列。 這個系列的函數是用於處理Key-sharing dict的。

Key-sharing dict是啥呢?其實就是key共享的dict。 這裏只介紹一些大概的內容, 具體的說明見PEP 412

Key-sharing dict的主要用做對象的__dict__屬性。 使用這種dict能夠把使同一類型的對象實例採用共享的key,從而節約空間。 因爲須要共享key,因此Key-sharing dict須要把值和key分離, 所以此類dict是split-table dict。

新建一個Key-sharing dict關鍵在於須要新建shared keys, 須要調用以下函數:

 
    
/* file:Objects/dictobject.c */
static PyDictKeysObject *
make_keys_shared(PyObject *op)
{
    Py_ssize_t i;
    Py_ssize_t size;
    PyDictObject *mp = (PyDictObject *)op;

    if (!PyDict_CheckExact(op))
        return NULL;
    if (!_PyDict_HasSplitTable(mp)) {
        /* 若不是Split Table,將其轉換爲split table */
        PyDictKeyEntry *ep0;
        PyObject **values;
        assert(mp->ma_keys->dk_refcnt == 1);
        if (mp->ma_keys->dk_lookup == lookdict) {
            return NULL;
        }
        else if (mp->ma_keys->dk_lookup == lookdict_unicode) {
            /* Remove dummy keys */
            if (dictresize(mp, DK_SIZE(mp->ma_keys)))
                return NULL;
        }
        assert(mp->ma_keys->dk_lookup == lookdict_unicode_nodummy);
        /* Copy values into a new array */
        ep0 = &mp->ma_keys->dk_entries[0];
        size = DK_SIZE(mp->ma_keys);
        values = new_values(size);
        if (values == NULL) {
            PyErr_SetString(PyExc_MemoryError,
                "Not enough memory to allocate new values array");
            return NULL;
        }
        for (i = 0; i < size; i++) {
            values[i] = ep0[i].me_value;
            ep0[i].me_value = NULL;
        }
        mp->ma_keys->dk_lookup = lookdict_split;
        mp->ma_values = values;
    }
    /* 增長ma_keys的引用並返回它 */
    DK_INCREF(mp->ma_keys);
    return mp->ma_keys;
}
 
    
 
   

這個函數的參數是一個dict類型的對象。 對於split-table dict,它直接返回ma_keys; 對於combined-table dict,它會嘗試把該dict新建一個split table 並把值從ma_keys中移動到split table中。 轉換過程當中進行了嚴格的檢查以確保轉換後能夠知足split table的條件。 若是轉換失敗,則返回NULL。

以後再調用new_dict_with_shared_keys便可產生一個新的Key-sharing dict。

3 dict的查找

python中提供了兩類默認搜索方法,一類針對combined-table dict, 另外一類針對split-table dict。

針對combined-table dict的查找中,有通用的lookdict, 還有針對key只爲string這種特例的lookdict_unicode, 也有限制更嚴格的lookdict_unicode_nodummy。 總的來講,它們的算法相似,區別只在於後兩個對於輸入數據的限制更嚴且作了一些針對性的優化。

3.1 lookdict

lookdict函數定義以下:

 
    
 1 /* file:Objects/dictobject.c */
 2 static PyDictKeyEntry *
 3 lookdict(PyDictObject *mp, PyObject *key,
 4          Py_hash_t hash, PyObject ***value_addr)
 5 {
 6     size_t i;
 7     size_t perturb;
 8     PyDictKeyEntry *freeslot;
 9     size_t mask;
10     PyDictKeyEntry *ep0;
11     PyDictKeyEntry *ep;
12     int cmp;
13     PyObject *startkey;
14 
15 top:
16     mask = DK_MASK(mp->ma_keys);
17     ep0 = &mp->ma_keys->dk_entries[0];
18     i = (size_t)hash & mask;
19     ep = &ep0[i];
20     if (ep->me_key == NULL || ep->me_key == key) {
21         *value_addr = &ep->me_value;
22         return ep;
23     }
24     if (ep->me_key == dummy)
25         freeslot = ep;
26     else {
27         if (ep->me_hash == hash) {
28             startkey = ep->me_key;
29             Py_INCREF(startkey);
30             cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
31             Py_DECREF(startkey);
32             if (cmp < 0)
33                 return NULL;
34             if (ep0 == mp->ma_keys->dk_entries && ep->me_key == startkey) {
35                 if (cmp > 0) {
36                     *value_addr = &ep->me_value;
37                     return ep;
38                 }
39             }
40             else {
41                 /* The dict was mutated, restart */
42                 goto top;
43             }
44         }
45         freeslot = NULL;
46     }
47 
48     /* In the loop, me_key == dummy is by far (factor of 100s) the
49        least likely outcome, so test for that last. */
50     for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {
51         i = (i << 2) + i + perturb + 1;
52         ep = &ep0[i & mask];
53         if (ep->me_key == NULL) {
54             if (freeslot == NULL) {
55                 *value_addr = &ep->me_value;
56                 return ep;
57             } else {
58                 *value_addr = &freeslot->me_value;
59                 return freeslot;
60             }
61         }
62         if (ep->me_key == key) {
63             *value_addr = &ep->me_value;
64             return ep;
65         }
66         if (ep->me_hash == hash && ep->me_key != dummy) {
67             startkey = ep->me_key;
68             Py_INCREF(startkey);
69             cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
70             Py_DECREF(startkey);
71             if (cmp < 0) {
72                 *value_addr = NULL;
73                 return NULL;
74             }
75             if (ep0 == mp->ma_keys->dk_entries && ep->me_key == startkey) {
76                 if (cmp > 0) {
77                     *value_addr = &ep->me_value;
78                     return ep;
79                 }
80             }
81             else {
82                 /* The dict was mutated, restart */
83                 goto top;
84             }
85         }
86         else if (ep->me_key == dummy && freeslot == NULL)
87             freeslot = ep;
88     }
89     assert(0);          /* NOT REACHED */
90     return 0;
91 }
 
    
 
   

函數的參數中,*value_addr是指向匹配slot中值的指針。 這個函數在正確的狀況下必定會返回一個指向slot的指針,出錯則會返回NULL。 若是成功找到了匹配的slot,則返回對應的slot; 若是沒有匹配的slot,則返回查找鏈上第一個未被使用的slot。 該slot能夠是unused狀態,也能夠是dummy狀態。

在函數的16~19行,計算了slot的初始位置,把hash值映射到slot table的下標範圍內。 初始位置=hash&mask,mask=dk_size-1。

20~22行,若是找到了匹配的key或unused slot,返回該結果便可。

24~45行進行了進一步的比較。 若該slot狀態爲dummy,則用freeslot記錄該slot並繼續搜索; 若是該slot的hash值與待搜索key的hash相同,那麼對兩個key進行比較。 這裏的PyObject_RichCompareBool是一個比較函數,其第三個參數爲比較的操做。 若是操做結果爲true,返回1;爲false,返回0;比較出錯,返回-1。 比較出錯的狀況下會返回NULL,比較成功(在這裏爲相等)返回該slot,比較不成功則繼續進行搜索。 這一部分進行了第一次的搜索;在dict容量不太滿時,通常在這裏就能夠找到合適的結果。

在50~88行則進行了接下來的搜索。 這裏須要計算下一個查找元素的下標: 對當前下標i,新的下標i=5*i+1+perturb,perturb是一個和hash值相關的數。 這樣計算的緣由能夠參考Objects/dictobject.c開頭的那段註釋。

53~61行是找到了unused slot的狀況。 若是freeslot是NULL,那麼返回該slot便可;若freeslot不是NULL,那麼返回freeslot。

62~65行則是找到了匹配的key。此狀況返回對應slot便可。

66~85行是該slot hash值與給定hash值相同時進一步比較的狀況。 這裏和前面的比較同樣,因此再也不說明了。

86~87行是在dummy狀況下設置freeslot。

在搜索過程當中,原則是找到和key相等的對象便可。 那麼什麼是和key相等呢? 一種狀況是它們的引用相等,天然的值也相等。 這類比較只須要直接比較對應指針是否相等呢該便可。 而另外一種狀況是引用不相等,但值還相等。 若是沒有對這種狀況的處理,那麼對於非共享的對象來講搜索幾乎不會獲得正確的結果。 搜索中的進一步比較就是對這種狀況的處理。 進一步比較發生的前提是hash值相等,由於值相等必然有hash相等, 但hash相等值卻可能不等,所以不能直接比較hash值,還須要更進一步的比較值才能夠。

3.2 lookdict_unicode

這個函數和lookdict的區別除了在於dict中key的類型有限制外, 還在於返回值不一樣。 lookdict可能會在比較失敗時返回NULL, 可此函數比較不會失敗,所以永遠不會返回NULL。

其定義以下:

 
    
/* file:Objects/dictobject.c */
static PyDictKeyEntry *
lookdict_unicode(PyDictObject *mp, PyObject *key,
                 Py_hash_t hash, PyObject ***value_addr)
{
    size_t i;
    size_t perturb;
    PyDictKeyEntry *freeslot;
    size_t mask = DK_MASK(mp->ma_keys);
    PyDictKeyEntry *ep0 = &mp->ma_keys->dk_entries[0];
    PyDictKeyEntry *ep;

    /* Make sure this function doesn't have to handle non-unicode keys,
       including subclasses of str; e.g., one reason to subclass
       unicodes is to override __eq__, and for speed we don't cater to
       that here. */
    if (!PyUnicode_CheckExact(key)) {
        mp->ma_keys->dk_lookup = lookdict;
        return lookdict(mp, key, hash, value_addr);
    }
    i = (size_t)hash & mask;
    ep = &ep0[i];
    if (ep->me_key == NULL || ep->me_key == key) {
        *value_addr = &ep->me_value;
        return ep;
    }
    if (ep->me_key == dummy)
        freeslot = ep;
    else {
        if (ep->me_hash == hash && unicode_eq(ep->me_key, key)) {
            *value_addr = &ep->me_value;
            return ep;
        }
        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) {
            if (freeslot == NULL) {
                *value_addr = &ep->me_value;
                return ep;
            } else {
                *value_addr = &freeslot->me_value;
                return freeslot;
            }
        }
        if (ep->me_key == key
            || (ep->me_hash == hash
            && ep->me_key != dummy
            && unicode_eq(ep->me_key, key))) {
            *value_addr = &ep->me_value;
            return ep;
        }
        if (ep->me_key == dummy && freeslot == NULL)
            freeslot = ep;
    }
    assert(0);          /* NOT REACHED */
    return 0;
}
 
   

這個函數和lookdict幾乎如出一轍,比較中惟一的區別在於比較時用了unicode_eq這個針對str對象的比較。 相似的,lookdict_unicode_nodummy和lookdict也是幾乎同樣的,這裏就再也不細說了。

3.3 lookdict_split

 1 /* file:Objects/dictobject.c */
 2 static PyDictKeyEntry *
 3 lookdict_split(PyDictObject *mp, PyObject *key,
 4                Py_hash_t hash, PyObject ***value_addr)
 5 {
 6     size_t i;
 7     size_t perturb;
 8     size_t mask = DK_MASK(mp->ma_keys);
 9     PyDictKeyEntry *ep0 = &mp->ma_keys->dk_entries[0];
10     PyDictKeyEntry *ep;
11 
12     if (!PyUnicode_CheckExact(key)) {
13         ep = lookdict(mp, key, hash, value_addr);
14         /* lookdict expects a combined-table, so fix value_addr */
15         i = ep - ep0;
16         *value_addr = &mp->ma_values[i];
17         return ep;
18     }
19     i = (size_t)hash & mask;
20     ep = &ep0[i];
21     assert(ep->me_key == NULL || PyUnicode_CheckExact(ep->me_key));
22     if (ep->me_key == NULL || ep->me_key == key ||
23         (ep->me_hash == hash && unicode_eq(ep->me_key, key))) {
24         *value_addr = &mp->ma_values[i];
25         return ep;
26     }
27     for (perturb = hash; ; perturb >>= PERTURB_SHIFT) {
28         i = (i << 2) + i + perturb + 1;
29         ep = &ep0[i & mask];
30         assert(ep->me_key == NULL || PyUnicode_CheckExact(ep->me_key));
31         if (ep->me_key == NULL || ep->me_key == key ||
32             (ep->me_hash == hash && unicode_eq(ep->me_key, key))) {
33             *value_addr = &mp->ma_values[i & mask];
34             return ep;
35         }
36     }
37     assert(0);          /* NOT REACHED */
38     return 0;
39 }

 

這個函數是專門用在split-table dict上的。 它的算法和lookdict_unicode_nodummy同樣, 區別就在於key對應的值的位置不一樣。

有一點須要注意的是,在12~18行中, 對於不符合條件的dict調用了lookdict。 由於lookdict設置的value_addr會指向slot內部的值, 因此以後還修改了value_addr指向的位置。

4 dict的維護

 

4.1 調整dict大小

在dict中插入元素可能致使dict大小的改變。 改變dict大小會使用dictresize函數。 它的定義以下:

 
    
 1 /* file:Objects/dictobject.c */
 2 static int
 3 dictresize(PyDictObject *mp, Py_ssize_t minused)
 4 {
 5     Py_ssize_t newsize;
 6     PyDictKeysObject *oldkeys;
 7     PyObject **oldvalues;
 8     Py_ssize_t i, oldsize;
 9 
10 /* Find the smallest table size > minused. */
11     for (newsize = PyDict_MINSIZE_COMBINED;
12          newsize <= minused && newsize > 0;
13          newsize <<= 1)
14         ;
15     if (newsize <= 0) {
16         PyErr_NoMemory();
17         return -1;
18     }
19     oldkeys = mp->ma_keys;
20     oldvalues = mp->ma_values;
21     /* Allocate a new table. */
22     mp->ma_keys = new_keys_object(newsize);
23     if (mp->ma_keys == NULL) {
24         mp->ma_keys = oldkeys;
25         return -1;
26     }
27     if (oldkeys->dk_lookup == lookdict)
28         mp->ma_keys->dk_lookup = lookdict;
29     oldsize = DK_SIZE(oldkeys);
30     mp->ma_values = NULL;
31     /* If empty then nothing to copy so just return */
32     if (oldsize == 1) {
33         assert(oldkeys == Py_EMPTY_KEYS);
34         DK_DECREF(oldkeys);
35         return 0;
36     }
37     /* Main loop below assumes we can transfer refcount to new keys
38      * and that value is stored in me_value.
39      * Increment ref-counts and copy values here to compensate
40      * This (resizing a split table) should be relatively rare */
41     if (oldvalues != NULL) {
42         for (i = 0; i < oldsize; i++) {
43             if (oldvalues[i] != NULL) {
44                 Py_INCREF(oldkeys->dk_entries[i].me_key);
45                 oldkeys->dk_entries[i].me_value = oldvalues[i];
46             }
47         }
48     }
49     /* Main loop */
50     for (i = 0; i < oldsize; i++) {
51         PyDictKeyEntry *ep = &oldkeys->dk_entries[i];
52         if (ep->me_value != NULL) {
53             assert(ep->me_key != dummy);
54             insertdict_clean(mp, ep->me_key, ep->me_hash, ep->me_value);
55         }
56     }
57     mp->ma_keys->dk_usable -= mp->ma_used;
58     if (oldvalues != NULL) {
59         /* NULL out me_value slot in oldkeys, in case it was shared */
60         for (i = 0; i < oldsize; i++)
61             oldkeys->dk_entries[i].me_value = NULL;
62         assert(oldvalues != empty_values);
63         free_values(oldvalues);
64         DK_DECREF(oldkeys);
65     }
66     else {
67         assert(oldkeys->dk_lookup != lookdict_split);
68         if (oldkeys->dk_lookup != lookdict_unicode_nodummy) {
69             PyDictKeyEntry *ep0 = &oldkeys->dk_entries[0];
70             for (i = 0; i < oldsize; i++) {
71                 if (ep0[i].me_key == dummy)
72                     Py_DECREF(dummy);
73             }
74         }
75         assert(oldkeys->dk_refcnt == 1);
76         DK_DEBUG_DECREF PyMem_FREE(oldkeys);
77     }
78     return 0;
79 }
 
    
 
   

此函數的輸入參數爲待操做的dict和新的最小大小。 不管是combined-table dict仍是split-table dict, 調用此函數後都會變成combined-table dict。 若是須要再恢復爲split-table dict, 只須要調用make_keys_shared便可。

在11~18行,尋找一個合適的新大小並進行內存檢查。這裏需再次注意,dict所佔的空間都是2的n次冪。

41~47行,把split table中的值轉義到combined table中。

50~55行,把全部處於active狀態的slot插入到新的combine table中。 爲何不把dummy slot也插入呢?這樣查找鏈不就斷了? 在執行insertdict_clean過程當中,會找到新的Unused slot,這個過程當中會重建查找鏈, 因此沒必要插入dummy slot。

57行維護了dk_usable值,減去了新dict已用的slot數。

58~65行,釋放了split table所佔空間。

66~77行則釋放了舊dict中ma_keys所佔據的空間, 同時維護dummy的引用數。

4.2 插入元素

 
    
 1 /* file:Objects/dictobject.c */
 2 static int
 3 insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
 4 {
 5     PyObject *old_value;
 6     PyObject **value_addr;
 7     PyDictKeyEntry *ep;
 8     assert(key != dummy);
 9 
10     if (mp->ma_values != NULL && !PyUnicode_CheckExact(key)) {
11         if (insertion_resize(mp) < 0)
12             return -1;
13     }
14 
15     ep = mp->ma_keys->dk_lookup(mp, key, hash, &value_addr);
16     if (ep == NULL) {
17         return -1;
18     }
19     Py_INCREF(value);
20     MAINTAIN_TRACKING(mp, key, value);
21     old_value = *value_addr;
22     if (old_value != NULL) {
23         assert(ep->me_key != NULL && ep->me_key != dummy);
24         *value_addr = value;
25         Py_DECREF(old_value); /* which **CAN** re-enter */
26     }
27     else {
28         if (ep->me_key == NULL) {
29             Py_INCREF(key);
30             if (mp->ma_keys->dk_usable <= 0) {
31                 /* Need to resize. */
32                 if (insertion_resize(mp) < 0) {
33                     Py_DECREF(key);
34                     Py_DECREF(value);
35                     return -1;
36                 }
37                 ep = find_empty_slot(mp, key, hash, &value_addr);
38             }
39             mp->ma_keys->dk_usable--;
40             assert(mp->ma_keys->dk_usable >= 0);
41             ep->me_key = key;
42             ep->me_hash = hash;
43         }
44         else {
45             if (ep->me_key == dummy) {
46                 Py_INCREF(key);
47                 ep->me_key = key;
48                 ep->me_hash = hash;
49                 Py_DECREF(dummy);
50             } else {
51                 assert(_PyDict_HasSplitTable(mp));
52             }
53         }
54         mp->ma_used++;
55         *value_addr = value;
56     }
57     assert(ep->me_key != NULL && ep->me_key != dummy);
58     assert(PyUnicode_CheckExact(key) || mp->ma_keys->dk_lookup == lookdict);
59     return 0;
60 }
 
    
 
   

insert操做的原理很簡單,經過lookdict函數找到合適的位置, 把須要插入的值填充進去並維護相關數值便可。

10~13行,對於不合條件的split-table dict調用resize使之變成combined table。

15~18行則在dict中查找出待插入元素的位置。

22~26行處理了替換元素的狀況,減小舊value的引用數。

27~56行則處理了非替換元素的插入。在key不是dummy時, 須要先檢查dk_usable以確保有效空間足夠; key是dummy時,插入元素便可。

5 Hack it

除了用前一篇list篇所寫那樣把輸出信息添加到str對象中的方法來輸出, 還有一種更省事的方法:直接打印信息。 只須要在str函數內添加對應的printf語句打印須要的內容便可很容易的打印出額外的信息。

相關文章
相關標籤/搜索