Python原始碼分析-PyDictObject

發表於2016-09-27

目前Cpython使用最多,下面分析下python中字典的原始碼實現

資料結構

1. PyDictObject

PyDictObject是python字典對應的C物件,本質上是一個hash表基本元素的組合,包含3個元素:

  • 一個table(可以看成是一個陣列)
  • hash函式
  • 表格中的每一項:entry

  1. PyDictObject包含了一個PyObject_HEAD, 任何python的物件都含他的指標。PyObject_HEAD包含一個雙向連結串列, 一個引用計算器, 一個物件描述(typeobject)。這個物件其實主要的作用是垃圾回收。
  2. ma_tablema_smalltable對應的是hash表中的table,但這裡為啥有兩個table呢?因為Python原始碼中使用了大量PyDictOject,但是dict中元素的數量一般比較少,為了方便,每次建立該物件時都會建立Pydict_MINISIZE個entry空間。當table中元素的個數超過一定數量時就會自動調整table的長度。所以,ma_table初始時等於ma_smalltable,當entry個數增加時,會調整 ma_table的長度。
  3. Py_ssize_t ma_mask是用於計算hash值的,它的值等於table的長度減一。這個屬性的理解非常重要,直接關係到是否能完全理Python的雜湊函式以及hash值的計算。Python字典的雜湊函式非常簡單,如下:
  4. ma_lookup 函式用於根據 key查詢 val。既然hash函式這麼簡單,那麼為什麼還需一個特殊的查詢函式呢?因為table中的entry不是簡單的一個數字或者字串,而是一個物件PyDictEntry,這個物件有自己的生命週期,所以i在查詢時稍微複雜一點。
  5. ma_fillma_used:上面說過PyDictEntry有自己的生命週期,包括3個狀態:unusedactive, dummy。ma_fill表示table中已使用的個數(=active+dummy),active表示當前正在使用的個數,dummy表示插入以後刪除的個數。

2. PyDictEntry

PyDictEntry是table中的具體元素項。

  1. me_hash是hash值, me_key是儲存的物件(可以是任意型別,因為python中一切皆物件,這些物件都是PyObject),me_value是儲存的值。

hash函式分析

理解一個hash表的實現,最重要的是理解其中的hash函式的實現,以及發生碰撞時的解決方法。

1. hash函式的實現

上面介紹過hash函式的實現

  1. PyDictObject本身的hash函式很簡單,因為key是經過一次hash的值,即get_key函式就是獲取一個物件(包括字串,整數和更復雜物件)的hash值。Python原始碼中的原型如下:
舉例分析怎麼獲取string物件的hash
  1. string物件的hash值獲取,先看string物件的定義
    • 每個string物件有一個 ob_shash,這個值就是該string的hash值。這個值就是通過tp_hash獲取的。具體可以參考原始碼Object/stringobject.c中的 static long string_hash()函式

綜上:hash函式進行雜湊之前,會先獲取每個物件的hash值,如果該物件有實現tp_hash函式,就呼叫該函式,如果沒有就使用該物件的記憶體地址的值作為hash值,然後用該值對 ma_mask取餘獲取該物件儲存到table中的位置。

2. 碰撞時的解決方式

hash雜湊發生碰撞的解決方法主要有:

  • 開放地址法,
  • 再雜湊法,
  • 鏈地址法等等。

python字典中使用的是再雜湊法,函式如下:

其中,perturb初始值是物件的hash值,

3. table大小的重新調整

什麼時候需要重新調整table的大小呢, hash表的效能主要表現在裝填因子上,

python的字典實現中,當裝填因子大於 2/3 時就進行重現調整table的大小,調整的過程其實就是新開闢一個計算得出的新大小的table空間,然後將舊table中的entry重新計算寫入新table中。

值得注意的是: 上面提到的fillma_fill(ma_fill=active+dummy)。也就是說這個裝填因子的計算考慮到了那些delete 的物件,就是刪除了,仍然計算在內。

PyDictObject物件的建立,插入與刪除

這部分內容比較簡單,直接看原始碼就行,後面再分析

相關文章