我們在學(xué)習(xí)理解一個事物時嚣鄙,往往遵循著由表及里的規(guī)律。第一步鳍怨,我們學(xué)習(xí)一個事物的特性表現(xiàn)(feature)呻右。在對事物的表現(xiàn)有了掌握以后,進(jìn)一步的鞋喇,我們嘗試探索事物表現(xiàn)背后的內(nèi)在原理声滥。本文在敘述過程中,也遵循這樣一個過程侦香。第一部分是對python對象系統(tǒng)feature的簡要概述落塑,第二部分則是對python 對象系統(tǒng)底層實(shí)現(xiàn)的梳理與總結(jié),在文章的最后一部分罐韩,是對常用的內(nèi)置python類型實(shí)現(xiàn)的一個介紹憾赁。
Python 中的對象與類型的行為表現(xiàn)
我們在學(xué)習(xí)python的過程中,比較經(jīng)常聽到的一句話叫做 散吵,python處處皆對象龙考。這是對于python對象模型的高度概括的描述。Python中的變量矾睦,實(shí)際上是一個名字晦款,指向所引用的對象。Python中有“名字空間”的概念枚冗,這類似于其他編程語言中的作用域缓溅。在python中,名字空間底層由一個dict實(shí)現(xiàn)赁温,變量名就是字典中的鍵肛宋,而變量引用的對象就是字典中鍵對應(yīng)的值。
那么現(xiàn)在我們知道束世,不同于其他的語言酝陈,python中的整數(shù),布爾值毁涉,字符串等類型的變量均是一個對象而不是簡單的值沉帮。更進(jìn)一步的,python中的所有語法元素,全部都是對象穆壕。函數(shù)是函數(shù)對象待牵,模塊是模塊對象,最特殊的是喇勋,對象的類型也是一個對象缨该。
Python中的面向?qū)ο髾C(jī)制與java等語言,所有的類型默認(rèn)繼承自基類object川背,或者某個object的派生類型贰拿。但是由于在python中類型也是一個對象,所以包含基類object在內(nèi)的所有類型熄云,都是對象實(shí)例膨更。區(qū)別于普通對象,我們姑且把描述類型的對象叫做 類對象缴允。所有類對象的類型被叫做元類(metaclass)荚守,元類默認(rèn)是一個type類對象,或者某個繼承自type的類對象练般。最后矗漾,type類對象的類型是type自身。
類對象總結(jié):
類對象 | 基類 | 類對象的類型 |
---|---|---|
普通類型 | object/object 派生類 | Type/type 派生類 |
元類 | type | type |
Object(默認(rèn)基類) | object | type |
Type(默認(rèn)元類) | object | type |
Python 底層對于對象與類型的實(shí)現(xiàn)
在對python語言層面上的對象與類型(對象)的行為表現(xiàn)有了了解以后薄料,下面我們來研究一下缩功,在python的底層是如何用c語言實(shí)現(xiàn)python對象與python類型的。
對象
python對象在python實(shí)現(xiàn)中都办,用PyObject實(shí)例來描述嫡锌。Python中的對象有很多種,int類型的對象對應(yīng)于PyIntObject實(shí)例琳钉,string類型的對象對應(yīng)于PyStringObject實(shí)例势木,但是這些PyxxxObject實(shí)例同時又都是PyObject實(shí)例,假設(shè)我們用c++去實(shí)現(xiàn)python的話歌懒,那么顯而易見的是啦桌,PyObject是一個基類,各種不同類型的對象實(shí)例描述都繼承自PyObject及皂。然而甫男,python是由c去實(shí)現(xiàn)的,沒有類與繼承的概念验烧,python的開發(fā)者通過精心設(shè)計(jì)結(jié)構(gòu)體的成員和布局模擬了繼承與多態(tài)板驳。首先泛化的PyObject結(jié)構(gòu)本身非常簡單,共有兩個成員ob_refcnt,和ob_type兩個成員碍拆,其結(jié)構(gòu)如下圖:
其中ob_refcnt 是對象的引用計(jì)數(shù)若治,而ob_type指向了對象的類型慨蓝。
我們再來看一個特化object結(jié)構(gòu),PyIntObject端幼。它由三個成員組成 ob_refcnt,ob_type,ob_val,其結(jié)構(gòu)如下圖:
可以看到礼烈,PyIntObject的頭,就是PyObject結(jié)構(gòu)婆跑,這意味著此熬,我們可以通過PyObject* 去引用一個PyIntObject實(shí)例。這就實(shí)現(xiàn)了在c++中通過基類指針去引用和訪問派生類對象的多態(tài)特性滑进。更進(jìn)一步的說犀忱,所有的python對象,都包含一個PyObject頭郊供,其中存儲著這個對象本身的引用計(jì)數(shù)和所述類型峡碉。不同類型的對象可能擁有不同類型的成員近哟,這些成員會存貯在對象內(nèi)存空間中PyObject頭后面的內(nèi)存里驮审。
Python 對象可以概括的分為兩種,定長對象和非定長對象吉执。定長對象指的是這類對象的數(shù)據(jù)成員長度全部相同疯淫,反之,兩個同類非定長對象的數(shù)據(jù)成員是可以不同的戳玫。顯然熙掺,Python int對象就是一個定長python對象判族,而python string對象就是非定長對象雀瓢。因?yàn)閮蓚€python string對象的長度是不相同的咆课。對于非定長的python對象磕蒲,其內(nèi)存頭部除了保存引用計(jì)數(shù)和類型之外各薇,還保存了該對象實(shí)例本身的數(shù)據(jù)長度ob_size渡冻。
定長對象結(jié)構(gòu):
變長對象結(jié)構(gòu):
類型
Python類型在python實(shí)現(xiàn)中踊兜,用PyTypeObject 實(shí)例來描述犹赖。Python類型有很多種试浙,每一種類型董瞻,都用一個PyTypeObject實(shí)例來描述。例如整數(shù)類型用PyInt_Type描述田巴,string類型用PyString_Type描述钠糊,PyTypeObject中主要存貯了特定Python類型的方法,方法以函數(shù)指針的形式存儲在PyTypeObject的結(jié)構(gòu)里壹哺。
首先應(yīng)該注意到的是抄伍,描述python類型的數(shù)據(jù)結(jié)構(gòu)中,竟然有一個非定長python對象頭管宵,這恰恰印證了逝慧,python類型本身也是對象的說法昔脯。仔細(xì)觀察pyTypeObject數(shù)據(jù)結(jié)構(gòu),其中大部分的成員是用于保存類型特殊方法的默認(rèn)的c語言實(shí)現(xiàn)的笛臣。舉例來說tp_new,對應(yīng)于new 的實(shí)現(xiàn)云稚,tp_init 對應(yīng)于init 的實(shí)現(xiàn)。關(guān)于類型的方法沈堡,在這里暫且按下不表静陈,我們首先關(guān)注pyTypeObject中的ob_type 和tp_bases兩個成員。Ob_type 成員用于指向一個python對象的python類型诞丽。那么作為類對象鲸拥,其類型就是元類,所以僧免,一個用于描述某種python類型的pyTypeObject實(shí)例刑赶,其Ob_type 指向的是這個python類型的元類所對應(yīng)的pyTypeObject實(shí)例。默認(rèn)情況下python類型的元類是type類型懂衩,其對應(yīng)的pyTypeObject實(shí)例為 pyType_Type撞叨。 tp_bases 指向一個數(shù)組,其中存儲了python類型的直接基類浊洞。如果一個python類型直接繼承自object(對應(yīng)的pyTypeObject 實(shí)例為pyBaseObject_Type)牵敷,那么其對應(yīng)的pyTypeObject實(shí)例的tp_bases 成員中只含有一個指向pyBaseObject_Type 對象的指針。
object與type
現(xiàn)在我們已經(jīng)知道了法希,在python類型中有兩個特殊的類型枷餐,object和type。在python底層分別用pyBaseObject_Type和pyType_Type兩個pyTypeObject實(shí)例描述苫亦。
Object和type的元類都是type毛肋,這意味著,pyBaseObject_Type和pyType_Type的obtype都指向pyType_Type屋剑。
Object和type的基類都是object润匙,這意味著,pyBaseObject_Type和pyType_Type的tb_bases中都有且只有一個指向pyBaseObject_Type的指針饼丘。
python對象系統(tǒng)小結(jié)
到此趁桃,對于python對象與python類型之間的基本關(guān)系可以算是梳理清楚了,在這個基礎(chǔ)之上肄鸽,之后還可以進(jìn)一步去探索探索自定義類型的創(chuàng)建卫病,類型實(shí)例化等內(nèi)容。但是在這之前典徘,這里我們用一個示意圖描述python對象與類型之間的關(guān)系蟀苛。
部分內(nèi)置python類型對象實(shí)現(xiàn)介紹
當(dāng)我們使用python進(jìn)行開發(fā)時,最長打交道的是各種內(nèi)置類型的對象逮诲。了解常用內(nèi)置類型的對象實(shí)例在python層是如何實(shí)現(xiàn)的帜平,有助于我們更加高效的使用python幽告。這一部分,我將整理python最常用的內(nèi)置類型的對象的底層實(shí)現(xiàn)裆甩。
int
Python 底層使用PyIntObject 結(jié)構(gòu)來描述python int 對象冗锁。PyIntObject是一個定長的python對象,其構(gòu)造十分簡單嗤栓,只含有一個成員ob_val冻河,保存的就是python int 實(shí)例中的數(shù)值。
從PyIntObject數(shù)據(jù)結(jié)構(gòu)可知茉帅,Python使用一個long 類型來保存數(shù)值叨叙。從日常使用python的經(jīng)驗(yàn)可以知道,python的整數(shù)類型是不用擔(dān)心溢出問題的堪澎,然而這里如果單單只用一個long類型去保存數(shù)值的話擂错,那么是可能出現(xiàn)溢出情況的。對于這種情況python底層做了透明的類型轉(zhuǎn)換樱蛤,一旦一個pyIntObject對象包含的數(shù)值會發(fā)生溢出钮呀,那么就重新創(chuàng)建一個pyLongObject 對象去保存數(shù)值。
對于常用的python內(nèi)置對象刹悴,另一個不得不提的事情是對象緩存機(jī)制行楞。由于python處處皆對象攒暇,這意味著為python內(nèi)置對象申請內(nèi)存空間的操作將會非常頻繁土匀。為了提高性能,針對于大部分常用的python內(nèi)置類型對象形用,python底層都有相應(yīng)的緩存措施就轧。
Python整數(shù)對象有兩個層次的緩沖措施。第一個措施是針對于小整數(shù)田度。在python2.7中妒御,
范圍在[-5,257]的整數(shù)對象,一旦創(chuàng)建將永久緩存在系統(tǒng)中镇饺。具體的存放位置是一個叫做small_ints的數(shù)組乎莉。針對于普通的整數(shù)對象,python設(shè)計(jì)了內(nèi)存塊空閑鏈表緩存所有malloc出的內(nèi)存并加以管理奸笤,這大大減少了由整數(shù)對象創(chuàng)建引發(fā)malloc的幾率惋啃。
long
Python 底層使用PyLongObject 結(jié)構(gòu)去描述python的大整數(shù)對象。PyLongObject內(nèi)部用一個數(shù)組去保存數(shù)值监右,因此PyLongObject是一個邊長對象.
在python源碼的注釋中,可以看到如何用這個數(shù)組去存儲一個大整數(shù)
“ The absolute value of a number is equal to SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2*(SHIFTi)”
bool
在python中边灭,Bool對象只有兩個對象實(shí)例Py_True 和Py_False。這是兩個PyIntObject對象健盒,數(shù)據(jù)成員的值分別為1和0绒瘦。
string
Python用PyStringObject 來描述字符串對象称簿,python字符串對象也是一個變長的數(shù)據(jù)對象。除了變長對象頭惰帽,字符串對象還包含了字符數(shù)組和字符串常量的哈希值憨降,此外對象內(nèi)部還有一個state標(biāo)志位用來標(biāo)識這個對象有沒有被python的intern緩存機(jī)制處理。
同整數(shù)對象一樣该酗,python的字符串對象也有兩種緩存機(jī)制券册。分別是針對單個字符的緩沖數(shù)組,和針對于所有字符串(包含字符)的intern機(jī)制垂涯。
Python中所有長度為1的字符串(字符)烁焙,一旦被創(chuàng)建,就會被永久的緩存到系統(tǒng)中耕赘,存儲在一個名字為characters的數(shù)組里骄蝇。
而python的字符串intern機(jī)制,保證了任何兩個字符串變量操骡,如果其字符串內(nèi)容相同九火,那么兩個名字引用的是用一個字符串對象。
list
Python用PyListObject來描述列表對象册招,PyListObject的數(shù)據(jù)結(jié)構(gòu)如下圖:
從上圖可以看出岔激,PyListObject看起來是一個變長的pyhon對象,從直觀理解上是掰,列表對象是變長的這個事情也是說的通的虑鼎,但是個人在這里傾向于認(rèn)為PyListObject是定長的列表對象。列表對象維護(hù)的數(shù)據(jù)對象都存儲在由ob_item指向的空間键痛,而這段空間與PyListOBject是分離的炫彩!所以PyListObject本身應(yīng)該是定長的。Ob_size在這里用于指出列表當(dāng)前有多少個對象絮短,allocated用于描述Ob_item所指向的內(nèi)存空間的總長度江兢。當(dāng)ob_size等于allocated時意味著內(nèi)存已滿,需要重新分配一塊內(nèi)存給這個列表對象丁频,這個過程與c++ vector容器是一致的杉允。
dict
Python 用PyDictObject來描述字典對象,PyDictObject的數(shù)據(jù)結(jié)構(gòu)如下圖:
可以看到席里,PyDictObject 一個定長的python對象叔磷,注意到PyDictObject中有兩個重要的成員,Ma_table 和Ma_smalltable胁勺。Ma_table 的作用和list對象中Ob_item的作用是一樣的世澜,指向的是真正保存字典元素的空間,當(dāng)字典中的元素個數(shù)小于8時署穗,這片空間就是Ma_smalltable數(shù)組寥裂!而當(dāng)字典元素大于8時嵌洼,系統(tǒng)會另外申請新的內(nèi)存空間,并將ma_smalltable的內(nèi)容拷貝過去封恰,最后使ma_table指向新的內(nèi)存空間麻养。
PyDictObject 保存的元素是一個pyDictEntry, 顯然這是一個鍵值對,其結(jié)構(gòu)如下
可以看到的是诺舔,無論是鍵還是值鳖昌,保存在PyDictObject中的都僅僅是一個指針。
PyDictObject除了作為python層dict對象的描述低飒,在python底層的實(shí)現(xiàn)中许昨,應(yīng)用也極為廣泛。例如string intern緩存褥赊,locals 命名空間都是用PyDictOBject實(shí)現(xiàn)的糕档。因此其對于搜索的效率要求很高。為此PydictObject采用了散列表的方式存儲元素以實(shí)現(xiàn)O(1) 復(fù)雜度的查找操作拌喉,而不是像c++ map容器一樣使用紅黑樹結(jié)構(gòu)速那。PythonDict容器對象的散列,采用的是開放定址法去實(shí)現(xiàn)尿背。當(dāng)產(chǎn)生散列沖突時端仰,通過二次探測函數(shù),發(fā)現(xiàn)下一個候選位置田藐。