詳解 Python 源碼之對象機制
本篇文章主要是對Python 進行詳解,內容是介紹Python 源碼之對象機制,先來看內容。
1、對象
在Python的世界中,一切都是對象,一個整數是一個對象,一個字符串也是一個對象,更為奇妙的是,類型也是一個對象,整數類型是一個對象,字符串類型也是一個對象。從1980年Guido在那個圣誕節揭開Python世界的大幕開始,一直到現在,Python經歷了一次一次的升級,但是其實現語言一直都是ANSI C。我們知道,C并不是一個面向對象的語言,那么在Python中,它的對象機制是如何實現的呢?
對于人的思維來說,對象是一個比較形象的概念,而對于計算機來說,對象實際上是一個抽象的概念。計算機并不能理解這是一個整數,那是一個字符串,對于計算機來說,它所知道的一切都是字節。通常的說法是,對象是數據以及基于這些數據的操作的集合。在計算機上,一個對象實際上就是一片被分配的內存空間,這些內存可能是連續的,也有可能是離散的,這都不重要,重要的是這片內存在更高的層次上可以作為一個整體來考慮,這個整體就是一個對象。在這片內存中,存儲著一系列的數據以及可以對這些數據進行修改或讀取的一系列操作的代碼。
在Python中,對象就是在堆上申請的結構體,對象不能是被靜態初始化的,并且也不能是在棧空間上生存的。唯一的例外就是類型對象(type object),Python中所有的類型對象都是被靜態初始化的。
在Python中,一個對象一旦被創建,它在內存中的大小就是不變的了。這就意味著那些需要容納可變長度數據的對象只能在對象內維護一個指向一個可變大小的內存區域的指針。為什么要設定這樣一條特殊的規則呢,因為遵循這樣的規則可以使通過指針維護對象的工作變得非常的簡單。因為一旦允許對象的大小可在運行期改變,我們可以考慮如下的情形。在內存中有對象A,并且其后緊跟著對象B。如果運行期某個時刻,A的大小增大了,這意味著必須將整個A移動到內存中的其他位置,否則A增大的部分將覆蓋原本屬于B的數據。一旦將A移動到內存中的其他位置,那么所有指向A的指針必須立即得到更新,光是想一想,就知道這樣的工作是多么的恐怖。
在Python中,所有的東西都是對象,而所有的對象都擁有一些相同的內容,這些內容在PyObject中定義,PyObject是整個Python對象機制的核心。
- [object.h]
- typedef struct _object {
- PyObject_HEAD
- } PyObject;
實際上,PyObject是Python中不包含可變長度數據的對象的基石,而對于包含可變長度數據的對象,它的基石是PyVarObject:
- [object.h]
- typedef struct { PyObject_VAR_HEAD} PyVarObject;
這兩個結構體構成了Python對象機制的核心基石,從代碼中我們可以看到,Python的對象的秘密都隱藏在PyObject_HEAD與PyObject_VAR_HEAD中。
- [object.h]
- #ifdef Py_TRACE_REFS
- /* Define pointers to support a doubly-linked list of all live heap objects. */
- #define _PyObject_HEAD_EXTRA \
- struct _object *_ob_next; \
- struct _object *_ob_prev;
- #define _PyObject_EXTRA_INIT 0, 0,#else#define _PyObject_HEAD_EXTRA#define _PyObject_EXTRA_INIT#endif
- /* PyObject_HEAD defines the initial segment of every PyObject. */
- #define PyObject_HEAD \
- _PyObject_HEAD_EXTRA \
- int ob_refcnt; \
- struct _typeobject *ob_type;#define PyObject_VAR_HEAD \
- PyObject_HEAD \
- int ob_size;
- /* Number of items in variable part */
在PyObject_HEAD中定義了每一個Python對象都必須有的內容,這些內容將出現在每一個Python對象所占有的內存的最開始的字節中,從PyObject_VAR_HEAD的定義可以看出,即使對于擁有可變大小數據的對象,其最開始的字節也含有相同的內容,這就是說,在Python中,每一個對象都擁有相同的對象頭部。這就使得在Python中,對對象的引用變得非常的統一,我們只需要用一個PyObject *就可以引用任意的一個對象,而不論該對象實際是一個什么對象。
在PyObject_HEAD的定義中,我們注意到有一個ob_refcnt的整形變量,這個變量的作用是實現引用計數機制。對于某一個對象A,當有一個新的PyObject *引用該對象時,A的引用計數應該增加;而當這個PyObject *被刪除時,A的引用計數應該減少。當A的引用計數減少到0時,A就可以從堆上被刪除,以釋放出內存供別的對象使用。
在PyObject_HEAD中,我們注意到ob_type是一個指向_typeobject結構體的指針,那么這個結構體是一個什么東西呢?實際上這個結構體也是一個對象,它是用來指定一個對象類型的類型對象。這個類型對象我們將在后邊詳細地考察。現在我們看到了,在Python中實際上對象機制的核心非常的簡單,一個是引用計數,一個就是類型。
而對于擁有可變長度數據的對象,這樣的對象通常都是容器,我們可以在PyObject_VAR_HEAD中看到ob_size這個變量,這個變量實際上就是指明了該對象中一共包含了多少個元素。注意,ob_size指明的是元素的個數,而不是字節的數目。比如對于Python中最常用的list,它就是一個PyVarObject對象,如果某一時刻,這個list中有5個元素,那么PyVarObject.ob_size的值就是5。
#p#
2、類型對象
在上面的描述中,我們看到了Python中所有對象的對象頭的定義。所以,當內存中存在某一個Python的對象時,該對象的開始的幾個字節的含義一定會符合我們的預期。但是,當我們把眼光沿著時間軸上溯,就會發現一個問題。當在內存中分配空間,創建對象的時候,毫無疑問地,必須要知道申請多大的空間。顯然,這不會是一個定值,因為對于不同的對象,需要不同的空間,一個整數對象和一個字符串對象所需的空間肯定不同。那么,對象所需的內存空間的大小的信息到底在哪里呢?在對象頭中顯然沒有這樣的信息。
實際上,內存空間大小這樣的對象的元信息是與對象所屬類型密切相關的,因此它一定會出現在與對象所對應的類型對象中。現在我們可以來詳細考察一下類型對象_typeobject:
- [object.h]
- typedef struct _typeobject {
- PyObject_VAR_HEAD char *tp_name;
- /* For printing, in format "<module>.<name>" */
- int tp_basicsize, tp_itemsize;
- /* For allocation */
- /* Methods to implement standard operations */
- destructor tp_dealloc;printfunc tp_print;
- ……
- /* More standard operations (here for binary compatibility) */
- hashfunc tp_hash;
- ternaryfunc tp_call;
- ……
- }
- PyTypeObject;
在_typeobject的定義中包含了許多的信息,主要可以分為四類:
1、類型名,tp_name,主要是Python內部以及調試的時候使用;
2、創建該類型對象是分配內存空間的大小的信息,即tp_basicsize和tp_itemsize;
3、與該類型對象相關聯的操作信息,比如hashfunc,tp_hash就指明對于該類型的對象,如何生成其hash值。在Object.h中可以看到,hashfunc實際上是一個函數指針:typedef long (*hashfunc)(PyObject *); 在_typeobject中,包含了大量的函數指針,這些函數指針將用來指定某個類型的操作信息。這些操作主要分為標準操作(dealloc, print, compare),標準操作族(numbers, sequences, mappings),以及其他操作(hash, buffer, call…)。
4、我們在下邊將要描述的類型的類型信息。
有趣的是我們在_typeobject的頭部發現了PyObject_VAR_HEAD,這意味著類型實際上也是一個對象。我們知道在Python中,每一個對象都是對應一種類型的,那么一個有趣的問題就出現了,類型對象的類型是什么呢?這個問題聽上去很繞口,實際上確非常重要,對于其他的對象,可以通過與其關聯的類型對象確定其類型,那么通過什么來確定一個對象是類型對象呢?答案就是PyType_Type:
- [typeobject.c]
- PyTypeObject PyType_Type = {
- PyObject_HEAD_INIT(&PyType_Type) 0,
- /* ob_size */ "type",
- /* tp_name */ sizeof(PyHeapTypeObject),
- /* tp_basicsize */ sizeof(PyMemberDef),
- /* tp_itemsize */ …… PyObject_GC_Del,
- /* tp_free */ (inquiry)type_is_gc,
- /* tp_is_gc */};
前面提到,在Python中,每一個對象它的開始部分都是一樣的。每一個對象都將自己的引用計數,類型信息保存在開始的部分中。為了方便對這部分內存的初始化,Python中提供了幾個有用的宏:
- [object.h]#ifdef Py_TRACE_REFS#define _PyObject_EXTRA_INIT 0, 0,#else#define _PyObject_EXTRA_INIT#endif#define PyObject_HEAD_INIT(type) \
- _PyObject_EXTRA_INIT \
- 1, type,
再回顧一下PyObject和PyVarObject的定義,初始化的動作就一目了然了。實際上,這些宏在類型對象的初始化中被大量地使用著。
如果以一個整數對象為例,可以更清晰地看到一半的類型對象和這個特立獨行的PyType_Type對象之間的關系:
- [intobject.c]
- PyTypeObject PyInt_Type = {
- PyObject_HEAD_INIT(&PyType_Type) 0,
- "int",
- sizeof(PyIntObject),
- ……
- };
現在我們可以放飛想象,看到一個整數對象在運行時的抽象的表示了,下圖中的箭頭表示ob_type:
#p#
3、對象間的繼承和多態
通過PyObject和類型對象,Python利用C語言完成了C++所提供的繼承和多態的特性。前面提到,在Python中所有的內建對象(PyIntObject等)和內部使用對象(PyCodeObject等)的最開始的內存區域都擁有一個PyObject。實際上,這一點可以視為PyIntObject,PyCodeObject等對象都是從PyObject繼承而來。
在Python創建一個對象,比如PyIntObject對象時,會分配內存,進行初始化。然后這個對象會由一個PyObject*變量來維護,而不是通過一個PyIntObject*指針來維護。其它對象也與此類似,所以在Python內部各個函數之間傳遞的都是一種范型指針PyObject*。這個指針所指的對象究竟是什么類型的,不知道,只能從指針所指對象的ob_type域判斷,而正是通過這個域,Python實現了多態機制。
考慮下面的代碼:
- void Print(PyObject* object)
- {
- object->ob_type->tp_print(object);
- }
如果傳給Print的指針實際是一個PyIntObject*,那么就會調用PyIntObject對象對應的類型對象中定義的輸出操作,如果傳給Print的指針實際是一個PyStringObject*,那么就會調用PyStringObject對象對應的類型對象中定義的輸出操作。可以看到,這里同一個函數在不同情況下表現出了不同的行為,這正是多態的核心所在。
在object.c中,Python實現了一些對于類型對象中的各種操作的簡單包裝,從而為Python運行時提供了一個統一的多態接口層:
- [object.c]
- long PyObject_Hash(PyObject *v)
- {
- PyTypeObject *tp = v->ob_type;
- if (tp->tp_hash != NULL)
- return (*tp->tp_hash)(v);
- ……
- }
4、引用計數
在C或C++中,程序員被賦予了極大的自由,可以任意地申請內存。但是權利的另一面則對應著責任,程序員必須自己負責將申請的內存釋放,并釋放無效指針。可以說,這一點正是萬惡之源,大量的內存泄露和懸空指針的bug由此而生,如黃河泛濫一發不可收拾 :)
現代的開發語言中一般都選擇由語言本身負責內存的管理和維護,即采用了垃圾收集機制,比如Java和C#。垃圾收集機制使開發人員從維護內存分配和清理的繁重工作中解放出來,但同時也剝奪了程序員與內存親密接觸的機會,并付出了一定的運行時效率作為代價。現在看來,隨著垃圾收集機制的完善,對時間要求不是非常高的程序完全可以通過使用垃圾收集機制的語言來完成,這部分程序占了這個星球上大多數的程序。這樣做的好處是提高了開發效率,并降低了bug發生的機率。Python同樣也內建了垃圾收集機制,代替程序員進行繁重的內存管理工作,而引用計數正式Python垃圾收集機制的一部分。
Python通過對一個對象的引用計數的管理來維護對象在內存中的生存。我們知道在Python中每一個東西都是一個對象,都有一個ob_refcnt變量,正是這個變量維護著該對象的引用計數,從而也最終決定著該對象的生生滅滅。
在Python中,主要是通過Py_INCREF(op)和Py_DECREF(op)兩個宏來增加和減少一個對象的引用計數。當一個對象的引用計數減少到0之后,Py_DECREF將調用該對象的析構函數(deallocator function)來釋放該對象所占有的內存和系統資源。注意這里的析構函數借用了C++的詞匯,實際上這個析構動作是通過在對象對應的類型對象中定義的一個函數指針來刻畫的,還記得嗎?就是那個tp_dealloc。
如果熟悉設計模式中Observer模式,可以看到,這里隱隱約約透著Observer模式的影子。在ob_refcnt減為0之后,將觸發對象銷毀的事件;從Python的對象體系來看,各個對象又提供了不同的事件處理函數,而事件的注冊動作正是在各個對象對應的類型對象中靜態完成的。
對于這兩個宏的參數op來說,不允許op是一個指向空對象的指針(NIL),如果op是一個NIL,那么必須使用Py_XINCREF/Py_XDECREF這一對宏。
在PyObject中我們看到ob_refcnt是一個32位的整形變量,這實際是一個Python所做的假設,即對一個對象的引用不會超過一個整形變量的最大值。一般情況下,如果不是惡意代碼,這個假設顯然是不會被突破的。
需要注意的是,在Python的各種對象中,類型對象是超越引用計數規則的。類型對象“跳出三界外,不再五行中”,永遠不會被析構。每一個對象中指向類型對象的指針不被視為對類型對象的引用。
在每一個對象創建的時候,Python提供了一個_Py_NewReference(op)宏來將對象的引用計數初始化為1。
在Python的源代碼中可以看到,在不同的編譯選項下(Py_REF_DEBUG, Py_TRACE_REFS),引用計數的宏還要做許多額外的工作。下面展示的代碼是Python在最終發行時這些宏所對應的實際的代碼:
- [object.h]
- /* Without Py_TRACE_REFS, there's little enough to do that we expand code
- * inline.
- */
- #define _Py_NewReference(op) ((op)->ob_refcnt = 1)
- #define _Py_Dealloc(op) ((*(op)->ob_type->tp_dealloc)((PyObject *)(op)))
- #define Py_INCREF(op) ((op)->ob_refcnt++)
- #define Py_DECREF(op) \
- if (--(op)->ob_refcnt != 0) \
- ; \
- else \
- _Py_Dealloc((PyObject *)(op))
- /* Macros to use in case the object pointer may be NULL: */
- #define Py_XINCREF(op) if ((op) == NULL) ; else Py_INCREF(op)
- #define Py_XDECREF(op) if ((op) == NULL) ; else Py_DECREF(op)
在一個對象的引用計數減為0時,與該對象對應的析構函數就會被調用,但是要特別注意的是,調用析構函數并不意味著最終一定會調用free釋放內存空間,如果真是這樣的話,那頻繁地申請、釋放內存空間會使Python的執行效率大打折扣(更何況Python已經多年背負了人們對其執行效率的指責:)。一般來說,Python中大量采用了內存對象池的技術,使用這種技術避免頻繁地申請和釋放內存空間。因此在析構時,通常都是將對象占用的空間歸還到內存池中。這一點在接下來對Python內建對象的實現中可以看得一清二楚。
#p#
5、Python對象的分類
我們將Python的對象從概念上大致分為四類,需要指出的是,這種分類并不一定完全正確,不過是提供一種看待Python中對象的視角而已:
Math :數值對象
Container :容納其他對象的集合對象
Composition :表示程序結構的對象
Internal :Python解釋器在運行時內部使用的對象
圖2列出了我們的對象分類體系,并給出了每一個類別中的一些實例:
6、通向Python之路
對Python源碼的剖析將分為四部分。
1.靜態對象剖析:首先我們會分析靜態的對象,Math對象和Container對象,深刻理解這些對象對我們理解Python解釋器的運行會有很大的幫助,同時,對我們編寫Python代碼也將大有裨益,在編寫Python代碼時,你會清晰地意識到系統內部這些對象將如何運作,變化。當然,我們并不會分析所有的Python對象,而是選取使用最頻繁的四種對象:PyIntObject, PyStringObject, PyListObject, PyDictObject進行剖析。
2.運行時剖析:在分析完靜態的對象之后,我們將進入Python解釋器,在這里我們會詳細地考察Python的字節碼(byte code)以及解釋器對字節碼的解釋和執行過程。這部分將完整地展現Python中所有的語法結構,如一般表達式,控制流,異常流,函數,類等等的字節碼層面的實現細節。同時,在這部分,我們會考察大部分的Python內部對象。
3.編譯期剖析:這部分沒什么好打廣告的了,目標明確,對象清晰,但是難度呢,絕不簡單 :
4.運行環境剖析:這部分將考察從激活Python到Python準備就緒,可以接受用戶輸入或者執行腳本文件,這段時間內,Python如何建立自己的運行環境,并建立了怎樣的運行環境,呵呵透露一下,想想Python那個龐大的builtin函數集合,這些就是這部分考察的重點。
閱讀完這些內容之后,對于Python,你應該是了如指掌了,在以后編寫Python代碼時,你的腦子里甚至可以出現Python解釋器將如何一步步解釋你的代碼的情形。當然,這只是我寫作本書的副產品。這本書誕生的真正原因只有一個,興趣,我對Python的實現有濃厚的興趣。這本書也只是第一步,希望以后還能繼續對Python系列,如IronPython、Jython,PyPy的探索,當然,對于其他動態語言,比如Ruby的探索,我希望也會有時間去做。
小結:詳解 Python 源碼之對象機制的內容介紹完了,希望本文對你有幫助,更多關于Python 的內容請參考編輯推薦。