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