Python對象初探(《Python源碼剖析》筆記一)

這是我的關於《Python源碼剖析》一書的筆記的第一篇。Learn Python by Analyzing Python Source Code · GitBook

Python對象初探

在Python中,一切皆對象。這一章中我們將會了解對象在C的層面究竟是如何實現的,同時還將了解到類型對象在C的層面是如何實現的。這樣,我們將對Python對象體系有一個大概的了解,從而進入具體的討論。

Python內的對象

在Python中,對象就是為C中的結構體在堆上申請的一塊內存。一般來說,對象是不能被靜態初始化的,並且也不能在棧空間上生存。唯一的例外是類型對象,Python中所有的內建類型對象都是被靜態初始化的。

在Python中,一個對象一旦被創建,它在內存中的大小就是不變的了。這就意味著那些需要容納可變長度數據的對象只能在對象內維護一個指向一塊可變大小的內存區域的指針。

一個對象維護著一個「引用計數」,其在一個指向這個對象的指針複製或刪除時增加或減少。當這個引用計數變為零時,也就是說已經沒有任何指向這個對象的引用,這個對象就可以從堆上被移除。

一個對象有著一個類型(type),來確定它代表和包含什麼類型的數據。一個對象的類型在它被創建時是固定的。類型本身也是對象。一個對象包含一個指向與之相配的類型的指針。類型自己也有一個類型指針指向著一個表示類型對象的類型的對象「type」,這個type對象也包括一個類型指針,不過是指向它自己的。

基本上Python對象的特性就是這些,那麼,在C的層面上,一個Python對象的這些特性是如何實現的呢?

對象機制的基石——PyObject

在Python中,所有的東西都是對象。這些對象都擁有著一些相同的內容,這些內容在PyObject中定義,所以PyObject是整個Python對象機制的核心。

[object.h]ntypedef struct _object {n _PyObject_HEAD_EXTRAn Py_ssize_t ob_refcnt;n struct _typeobject *ob_type;n} PyObject;n

#ifdef Py_TRACE_REFSn/* Define pointers to support a doubly-linked list of all live heap objects. */n#define _PyObject_HEAD_EXTRA n struct _object *_ob_next; n struct _object *_ob_prev;nn#define _PyObject_EXTRA_INIT 0, 0,nn#elsen#define _PyObject_HEAD_EXTRAn#define _PyObject_EXTRA_INITn#endifn

從上面的代碼中,我們可以看到前面我們提到的對象的特性的實現:

變數ob_refcnt與Python的內存管理機制有關,它實現了基於引用計數的垃圾收集機制。對於某一個對象A,當有一個新的PyObject *引用該對象時,A的引用計數應該增加;而當這個PyObject *被刪除時,A的引用計數應該減少。當A的引用計數減少到0時,A就可以從堆上被刪除,以釋放出內存供別的對象使用。

在ob_refcnt之外,我們注意到ob_type是一個指向_typeobject結構體的指針。這個結構體是用來指定一個對象類型的類型對象。 所以,我們可以看到,在Python中,對象機制的核心十分簡單——引用計數和類型信息。 PyObject中定義了每個Python對象中都會有的東西,它們將出現在每個對象所佔有的內存的最開始的位元組中。不過,這可不代表每個Python對象就有這麼點東西,事實上,除此之外,每個對象還會根據自己本身類型的不同而包括著不同的內容。

定長對象和變長對象

在Python2中,一個int類型的對象的值在C中的類型是不變的(int),所以它在內存中佔據的大小也是不變的。但是一個字元串對象事先我們根本不可能知道它所維護的值的大小。在C中,根本就沒有「一個字元串」的概念,字元串對象應該維護「n個char型變數」。不只是字元串,list等對象也應該維護「n個PyObject對象」。這種「n個……」也是一類Python對象的共同特徵,因此,Python在PyObject之外,還有一個表示這類對象的結構體——PyVarObject:

[object.h]ntypedef struct {n PyObject ob_base;n Py_ssize_t ob_size; /* Number of items in variable part */n} PyVarObject;n

我們把類似Python2中的int對象這樣不包含可變長度的對象稱為「定長對象」,而字元串對象這樣包含可變長度數據的對象稱為「變長對象」。

為什麼要強調Python2中的int對象呢?因為在Python3中,int類型的底層實現直接使用了Python2中的long類型的底層實現,也就是說,現在的int是以前的long類型,而以前的int類型已經不復存在。而long類型實際是一個變長對象。

變長對象通常都是容器,ob_size這個成員實際上就是指明了變長對象中一共容納了多少個元素。

從PyVarObject的定義可以看出,變長對象實際就是在PyObject對象後面加了個ob_size,因此,對於任意一個PyVarObject,其所佔用的內存開始部分的位元組就是一個PyObject。在Python內部,每一個對象都擁有著相同的對象頭部。這就使得在Python中,對對象的引用變得非常的統一,我們只需要用一個PyObject *指針就可以引用任意的一個對象。

類型對象

我們前面提到,一個對象就是在內存中維護的一塊內存。顯然,對於不同的對象,它在內存中佔用的大小是不同的,但在PyObject的定義中我們卻未見到關於這方面的信息。實際上,佔用內存空間的大小是對象的一種元信息,這樣的元信息是與對象所屬類型密切相關的,因此它一定會出現在與對象所對應的類型對象中。這個類型對象就是_typeobject。

[object.h]ntypedef struct _typeobject {n PyObject_VAR_HEADn const char *tp_name; /* For printing, in format "<module>.<name>" */n Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */nn /* Methods to implement standard operations */nn destructor tp_dealloc;n printfunc tp_print;n getattrfunc tp_getattr;n setattrfunc tp_setattr;n PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)n or tp_reserved (Python 3) */n reprfunc tp_repr;nn /* Method suites for standard classes */nn PyNumberMethods *tp_as_number;n PySequenceMethods *tp_as_sequence;n PyMappingMethods *tp_as_mapping;nn /* More standard operations (here for binary compatibility) */nn hashfunc tp_hash;n ternaryfunc tp_call;n reprfunc tp_str;n getattrofunc tp_getattro;n setattrofunc tp_setattro;nn /* Functions to access object as input/output buffer */n PyBufferProcs *tp_as_buffer;nn /* Flags to define presence of optional/expanded features */n unsigned long tp_flags;nn const char *tp_doc; /* Documentation string */nn /* Assigned meaning in release 2.0 */n /* call function for all accessible objects */n traverseproc tp_traverse;nn /* delete references to contained objects */n inquiry tp_clear;nn /* Assigned meaning in release 2.1 */n /* rich comparisons */n richcmpfunc tp_richcompare;nn /* weak reference enabler */n Py_ssize_t tp_weaklistoffset;nn /* Iterators */n getiterfunc tp_iter;n iternextfunc tp_iternext;nn /* Attribute descriptor and subclassing stuff */n struct PyMethodDef *tp_methods;n struct PyMemberDef *tp_members;n struct PyGetSetDef *tp_getset;n struct _typeobject *tp_base;n PyObject *tp_dict;n descrgetfunc tp_descr_get;n descrsetfunc tp_descr_set;n Py_ssize_t tp_dictoffset;n initproc tp_init;n allocfunc tp_alloc;n newfunc tp_new;n freefunc tp_free; /* Low-level free-memory routine */n inquiry tp_is_gc; /* For PyObject_IS_GC */n PyObject *tp_bases;n PyObject *tp_mro; /* method resolution order */n PyObject *tp_cache;n PyObject *tp_subclasses;n PyObject *tp_weaklist;n destructor tp_del;nn /* Type attribute cache version tag. Added in version 2.6 */n unsigned int tp_version_tag;nn destructor tp_finalize;nn#ifdef COUNT_ALLOCSn /* these must be last and never explicitly initialized */n Py_ssize_t tp_allocs;n Py_ssize_t tp_frees;n Py_ssize_t tp_maxalloc;n struct _typeobject *tp_prev;n struct _typeobject *tp_next;n#endifn} PyTypeObject;n

可以看出,在_typeobject的定義中包含了許多的信息,主要可分為四類:

  • 類型名,tp_name,主要是Python內部和調試的時候使用。

  • 創建該類型對象時分配內存空間大小的信息,即tp_basicsize和tp_itemsize。

  • 與該類型對象相關聯的操作信息。

  • 類型的類型信息。

對象的創建

假如我們命令Python創建一個整數對象,Python內部究竟如何創建呢?

一般來說,Python會有兩種方法。第一種是通過Python C API來創建,第二種是通過類型對象PyLong_Type。(在Python3中,已經沒有了long類型,int類型的底層實現都是通過以前的long類型來實現的)

Python的C API分成兩類:

一類稱為範式的API,或者稱為AOL(Abstract Object Layer)。這類API都具有諸如PyObject__**的形式,可以應用在任何Python對象身上。對於創建一個整數對象,我們可以採用如下的表達式:PyObject_longObj = PyObject_New(PyObject,&PyLong_Type)。

另一類是與類型相關的API,或者稱為COL(Concrete Object Layer)。這類API通常只能作用在某一種類型的對象上,對於每一種內建對象,Python都提供了這樣的一組API。比如整數對象,我們可以使用下列的API來創建,PyObject* longObj = PyLong_FromLong(10)。

不論採用那種C API,Python內部最終都是直接分配內存。但是對於用戶自定義的類型,如果要創建它的實例對象,Python就不可能事先提供類似Py_New這樣的API。對於這種情況,Python會通過它所對應的類型對象來創建實例對象。所有的類都是以object為基類的。

對象的行為

在PyTypeObject中定義了大量的函數指針,這些函數指針最終都會指向某個函數,或者指向NULL。這些函數指針可以視為類型對象中所定義的操作,而這些操作直接決定著一個對象在運行時所表現出的行為。

在這些操作信息中,有三組非常重要的操作族,在PyTypeObject中,它們是tp_as_number、tp_as_sequence、tp_as_mapping。它們分別指向PyNumberMethods、PySequenceMethods和PyMappingMethods函數族。

我們可以看看PyNumricMethods函數族:

typedef PyObject * (*binaryfunc)(PyObject *, PyObject *);nntypedef struct {n /* Number implementations must check *both*n arguments for proper type and implement the necessary conversionsn in the slot functions themselves. */nn binaryfunc nb_add;n binaryfunc nb_subtract;n ……n} PyNumberMethods;n

可以看出,一個函數族中包括著一族函數指針,它們指向的函數定義了作為一個數值對象所應該支持的操作。

對於一種類型來說,它完全可以同時定義三個函數族中的所有操作。

類型的類型

類型對象的類型是PyType_Type。

[typeobject.c]nPyTypeObject PyType_Type = {n PyVarObject_HEAD_INIT(&PyType_Type, 0)n "type", /* tp_name */n sizeof(PyHeapTypeObject), /* tp_basicsize */n sizeof(PyMemberDef), /* tp_itemsize */n (destructor)type_dealloc, /* tp_dealloc */n 0, /* tp_print */n 0, /* tp_getattr */n 0, /* tp_setattr */n 0, /* tp_reserved */n (reprfunc)type_repr, /* tp_repr */n 0, /* tp_as_number */n 0, /* tp_as_sequence */n 0, /* tp_as_mapping */n 0, /* tp_hash */n (ternaryfunc)type_call, /* tp_call */n 0, /* tp_str */n (getattrofunc)type_getattro, /* tp_getattro */n (setattrofunc)type_setattro, /* tp_setattro */n 0, /* tp_as_buffer */n Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |n Py_TPFLAGS_BASETYPE | Py_TPFLAGS_TYPE_SUBCLASS, /* tp_flags */n type_doc, /* tp_doc */n (traverseproc)type_traverse, /* tp_traverse */n (inquiry)type_clear, /* tp_clear */n 0, /* tp_richcompare */n offsetof(PyTypeObject, tp_weaklist), /* tp_weaklistoffset */n 0, /* tp_iter */n 0, /* tp_iternext */n type_methods, /* tp_methods */n type_members, /* tp_members */n type_getsets, /* tp_getset */n 0, /* tp_base */n 0, /* tp_dict */n 0, /* tp_descr_get */n 0, /* tp_descr_set */n offsetof(PyTypeObject, tp_dict), /* tp_dictoffset */n type_init, /* tp_init */n 0, /* tp_alloc */n type_new, /* tp_new */n PyObject_GC_Del, /* tp_free */n (inquiry)type_is_gc, /* tp_is_gc */n};n

所有用戶自定義class所對應的PyTypeObject對象都是通過這個對象創建的。

在Python層面,這個神奇的對象PyType_Type被稱為metaclass。

Python對象的多態性

在Python創建一個對象,比如PyLongObject對象,會分配內存,進行初始化。然後Python內部會用一個PyObject*變數,而不是通過一個PyLongObject*變數來保存和維護這個對象。其他對象也類似,所以在Python內部各個函數之間傳遞的都是一種范型指針——PyObject*。這個指針所指的對象究竟是什麼類型的,我們只能從指針所指對象的ob_type域動態判斷,而正是通過這個域,Python實現的多態機制。

這樣我們就能夠理解,一個Python對象在程序運行時可以動態改變它的類型。也就是說,一個實例對象x的代表著它的類型的魔法屬性__class__並不是只讀的,而是可以動態改變的。

引用計數

Python通過對一個對象的引用計數的管理來維護對象在內存中的存在與否。在Python中,主要是通過Py_INCREF(op)和Py_DECREF(op)兩個宏來增加和減少一個對象的引用計數。當一個對象的引用計數減少到0之後,Py_DECREF將調用該對象的析構函數(類型對象中定義的一個函數指針——tp_dealloc)來釋放該對象的內存和系統資源。

在Python的各種對象中,類型對象是超越引用計數規則的。類型對象永遠不會被析構。每一個對象中指向類型對象的指針不被視為對類型對象的引用。

在每一個對象創建的時候,Python提供了一個_Py_NewReference(op)宏來將對象的引用計數初始化為1。

在一個對象的引用計數減為0時,與該對象對應的析構函數就會被調用,但要注意的是,調用析構函數並不意味著最終一定會調用free釋放內存空間。一般來說,Python中大量採用了內存對象池的技術,使用這種技術可以避免頻繁地申請和釋放內存空間。因此在析構時,通常都是將對象佔用的空間歸還到內存池中。

Python對象的分類

  • Fundamental對象:類型對象

  • Numeric對象:數值對象

  • Sequence對象:容納其他對象的序列集合對象

  • Mapping對象:類似C++中map的關聯對象

  • Internal對象:Python虛擬機在運行時內部使用的對象。

推薦閱讀:

用Cython和PyPy提升Python性能
為什麼Cython寫的擴展會比直接用C寫還快?

TAG:Python | C编程语言 | Cython |