Python物件初探(《Python原始碼剖析》筆記一)

鬆直發表於2019-01-30
這是我的關於《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]
typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;複製程式碼
#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複製程式碼

從上面的程式碼中,我們可以看到前面我們提到的物件的特性的實現:

變數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]
typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;複製程式碼

我們把類似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]
typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    printfunc tp_print;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;

    /* Method suites for standard classes */

    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More standard operations (here for binary compatibility) */

    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;

    destructor tp_finalize;

#ifdef COUNT_ALLOCS
    /* these must be last and never explicitly initialized */
    Py_ssize_t tp_allocs;
    Py_ssize_t tp_frees;
    Py_ssize_t tp_maxalloc;
    struct _typeobject *tp_prev;
    struct _typeobject *tp_next;
#endif
} PyTypeObject;複製程式碼

可以看出,在_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 *);

typedef struct {
    /* Number implementations must check *both*
       arguments for proper type and implement the necessary conversions
       in the slot functions themselves. */

    binaryfunc nb_add;
    binaryfunc nb_subtract;
    ……
} PyNumberMethods;複製程式碼

可以看出,一個函式族中包括著一族函式指標,它們指向的函式定義了作為一個數值物件所應該支援的操作。

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

型別的型別

型別物件的型別是PyType_Type。

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

所有使用者自定義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虛擬機器在執行時內部使用的物件。

相關文章