python内置对象的实现_Python 内置对象的实现
準備回顧一下python源代碼,不過不準備說的太細,盡量勾勒框架,不引用代碼。
python中所有東西都是對象,進一步地,這些對象可以分為類型對象(type)or實例對象,有時一個對象即可以是類型,也可以是實例。所有這些對象中,除了內置的類型對象外,別的都生存于堆上,內置的類型對象則靜態分配內存。
每個對象頭部都有一個PyObject_HEAD(其實對于某些需要被gc管理的對象,它的頭部先為PyGC_Head,再為PyObject_HEAD)。變長對象在HEAD后還有一個ob_size表示變長對象元素個數的多少,非字節數。
類型的信息都在它的type對象里,源碼中為struct _typeobject,也就是PyTypeObject。比如實例化一個類型,那會先找它的tp_new(找不到的話在父類找),在tp_new中根據該type的tp_basesize進行分配內存,再調用tp_init進行初始化。對類型的實例做運算,比如相加其實也是找type對象中相應的函數指針。type對象中的信息到后來基本都會在類型的dict中和相應的key對應起來。
下面分析具體的類型。
int:比較簡單,關鍵在于如何高效地實現。python首先有小整數對象。默認在[-5, 257)。如果超出范圍則使用通用的緩沖池,對于大整數則有PyIntBlock,用來作緩沖池。一個block大小大概為1000個字節,去掉頭部(8字節),可以存82個整數對象。block之間通過指針相連,首指針為block_list,free_list則維護著一條可以鏈表,free_list鏈表的下一項由未用的PyIntObject的ob_type來維持。
一些細節:當無可以用緩沖池可用時python會調用fill_free_list來創建一個新的block,并將其插入block_list,再把free_list指向這個block的objects中的最后一個元素。當某個block中的某個int被釋放時,它將自己的ob_type指向free_list,并修改free_list等于它的地址,其實就是一個頭部插入,這樣把多個block間的objects數組聯系起來防止出現內存泄漏。一個值得注意的地方是小整數對象池其實也是生活在block里面,在是整個python環境初始化的時候生成。這里可以看出,為int分配的內存是永遠也不會被python釋放的,所有的int對象使用的內存大小和同時存在的int數量的最大值有關。
string:復雜一些,變長對象。對變長對象內存的管理。每個string對象除了頭部外還保存了hash值(ob_shash,避免重復計算,初始-1)、是否已經被intern機制處理過(ob_sstate)、指向實際內存的指針(ob_sval),ob_sval指向的應該是一段ob_size+1長度的內存(為了兼容C,字符串要以'\0'結尾)。在從char *創建string時還是比較直接的,就是檢查一些邊界情況、初始化hash等,最后逐個拷貝char。python中有一個nullstring指向空字符串,通過intern機制共享,所以不會同時存在多個空字符串。
傳統緩沖池。相當于int小整數緩沖池,對單個的char,python也會維持一個緩沖池。創建單個char的string時,如果緩沖池里已有,則直接返回。如果沒有,根據char創建string,再對它進行intern,再存入緩沖池。
intern機制。python會維持一個dict,用來保存當前已經被創建的string,如果新創建的string已經在這個dict,也就是已經被intern機制處理過了,那么就會直接返回dict中的值。也就是說一般兩個相同的字符串的id是相同的。要注意的是,無論字符串有沒有存在于這個dict中,python都會創建一個新要string,原因是因為保存在dict中只能是PyObject,因此肯定要創建一個python對象。intern后的string有兩種狀態,mortal和immortal,區別在于后者永遠不會被gc回收。
要提到的是,創建string時使用的是PyObject_Malloc開頭的分配函數,一般來講它不會每次都從os分配內存,而是從python維持的一個內存池中分配。
list:不僅是變長對象,還是可變對象。在變長頭部之外PyListObject還保存了一個PyObject **ob_item指針,一個int allocated。ob_item就是指向實際成員的指針,allocated代表了list當前申請的內存能裝多少個PyObject,變長頭內的ob_size則代表list中已有多少個PyObject。當創建一個list時,需要指定list的大小(參數size),要申請的內存可以分為兩部分,一個是list本身,一個是指向成員變量的指針。如果list緩沖池可用(numfree > 0),那就從緩沖池中給list分配一塊內存,否則使用PyObject_GC_NEW來分配空間(和string不同,因為list中的成員可以指向其它python對象,這個函數和python垃圾回收的三色標記法有關)。然后再根據size大小分配一段連續的內存來保存指向各個成員的指針,新創建的list中的ob_size和allocated都為size。
創建list后給list里的某個位置賦值比較簡單,就是簡單的設定指針而已。添加操作要復雜一些,首先會調整list的大小,使得allocate>=size>=allocate/2。如果該等式已經滿足,那么不更改list大小,如果不滿足的話通過new_allocated = (newsize >> 3) + (newsize < 9 ? 3 : 6) + newsize得出新的allocated大小,再通過PyMem_Resize來調整保存成員指針的內存。resize后則使用最簡單的策略移動從插入到結尾的成員指針,再設置該位置上的成員。刪除成員時實際刪除后的也要進行resize操作,和插入時類似。實際刪除的操作則由list_ass_slice來實現,它有兩種用法,一是更改list中的某一段為特定的值,另一種就是刪除list中的某一段。list中每刪除一個元素都會造成內存拷貝。一個值得注意的細節是由于從list中刪除或是更改的對象需要減少引用計數,但減少引用計數時又會循環調用一些list函數,可能會造成list索引值的破壞,因而函數中得用一個temp數組保留從list中剝離的成員,等刪除工作完成,list結構已經確定的情況下再逐一減少其引用。
當list被銷毀時,對其成員逐個減少引用,然后檢查緩沖區是否已經滿,如果沒有的話就將該list放入緩沖區,這樣下次再創建list的時候就不用為list對象本身再分配對象了。
dict:python中的dict是用hash表實現,解決沖突的方法是開放定址法,即二次探測,刪除時使用偽刪除技術(順便說下在一致性假設下,如果用k來表示hash表的使用率的話,那么一次成功查找需要的比較次數為1/k * ln(1/(1-k)),插入時的比較次數最多為1/(1-k))。
在一個dict中,每個(key,value)對組成一個entry,entry中還另外存儲了key的hash值。對每個entry來說有三種狀態unused(key,value皆為NULL,初始狀態),active(key,value不為NULL,存儲了元素),dump(對應于偽刪除技術,key=dummy,value=NULL)。dict實際上就是entry的集合,定義如下:typedef struct _dictobject PyDictObject;
struct _dictobject {
PyObject_HEAD
Py_ssize_t ma_fill;? /* # Active + # Dummy */
Py_ssize_t ma_used;? /* # Active */
/* The table contains ma_mask + 1 slots, and that's a power of 2.
* We store the mask instead of the size because the mask is more
* frequently needed.
*/
Py_ssize_t ma_mask;
/* ma_table points to ma_smalltable for small tables, else to
* additional malloc'ed memory.? ma_table is never NULL!? This rule
* saves repeated runtime null-tests in the workhorse getitem and
* setitem calls.
*/
PyDictEntry *ma_table;
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
PyDictEntry ma_smalltable[PyDict_MINSIZE];
};
PyDict_MINSIZE一般被設為8。使用PyDict_New來創建一個新dict時,其實就是分配相應的內存,將ma_fill,ma_used設為0,ma_mask設為7(8-1),ma_table指向ma_smalltable,ma_lookup默認為lookdict_string。在實現dict時python也使用了緩沖池,方法和list基本一樣。
ma_lookup這個函數指針確定了散裂函數和沖突發生時的二次散裂函數,在dict中進行搜索有兩種方法,lookdict_string和lookdict,后一種是更一般的方法。由于python中dict用的非常廣泛,而這些dict中大多數key都是string,因而專門提供了一個搜索方法來進行加速,其實兩種方法的本質都是一樣的。搜索的具體過程如下:1. 獲得探測鏈中當前應該檢測的entry;2. 如果是unused狀態,那搜索失敗,應該返回一個可用的(可以被設置值)的entry,所以如果freeslot不為空(之前找到了dummy態的entry)就返回freeslot指向的entry,否則返回當前entry;3. 如果entry為dummy態且freeslot未設置,則設置freeslot,繼續查找下一個;4. 依次檢查當前entry的key和查找的key是否引用相同、值相同,若不是繼續查找下一個。需要注意的是,dict使用哪種方法進行查找取決于待查找的key,如果是string的話則利用lookdict_string,和本身已經有的entry中的key無關。
dict的插入和刪除基于之前的搜索,很好理解。當插入時先搜索該key,得到一個entry,再根據它的狀態來修改它達到插入的上目的。刪除操作也類似。需要注意的是插入元素的時候,會檢查ma_fill/(ma_mask+1)是否大于2/3,如果裝載率的確大于這個值并且有unused或dummy的entry被填充的時候,就會調整dict的大小,新的大小最小為minsize=ma_userd*(ma_used>50000?2:4),顯然改變dict大小會造成內存移動,因此這時候可以丟棄dummy的entry。新的dict大小從8開始逐次*2增長,直到大于minsize。接下來就是一些初始化動作,逐個檢查entry并插入新的dict等。
總結
以上是生活随笔為你收集整理的python内置对象的实现_Python 内置对象的实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python中options设置_如何使
- 下一篇: python函数能否增强代码可读性_总结