通過 PyObject 和 PyVarObject,我們看到了所有對象的公共信息以及變長對象的公共信息。任何一個對象,不管它是什么類型,內(nèi)部必有引用計(jì)數(shù)(ob_refcnt)和類型指針(ob_type)。任何一個變長對象,不管它是什么類型,內(nèi)部除了引用計(jì)數(shù)和類型指針之外,還有一個表示元素個數(shù)的 ob_size。
顯然目前沒有什么問題,一切都是符合預(yù)期的,但是當(dāng)我們順著時間軸回溯的話,就會發(fā)現(xiàn)端倪。比如:
想都不用想,這些信息肯定都在對象的類型對象中。因?yàn)檎加玫目臻g大小實(shí)際上是對象的一個元信息,這樣的元信息和其所屬類型是密切相關(guān)的,因此它一定會出現(xiàn)在與之對應(yīng)的類型對象當(dāng)中。
至于支持的操作就更不用說了,我們平時自定義類的時候,功能函數(shù)都寫在什么地方,顯然都是寫在類里面,因此一個對象支持的操作也定義在類型對象當(dāng)中。
而將對象和它的類型對象關(guān)聯(lián)起來的,毫無疑問正是該對象內(nèi)部的 PyObject 的 ob_type 字段,也就是類型指針。我們通過對象的 ob_type 字段即可獲取類型對象的指針,然后通過指針獲取存儲在類型對象中的某些元信息。
下面我們來看看類型對象在底層是怎么定義的。
PyObject 的 ob_type 字段的類型是 PyTypeObject *,所以類型對象由 PyTypeObject 結(jié)構(gòu)體負(fù)責(zé)實(shí)現(xiàn),看一看它長什么樣子。
// Include/pytypedefs.htypedef struct _typeobject PyTypeObject;// Include/cpython/object.hstruct _typeobject { PyObject_VAR_HEAD const char *tp_name; Py_ssize_t tp_basicsize, tp_itemsize; destructor tp_dealloc; Py_ssize_t tp_vectorcall_offset; getattrfunc tp_getattr; setattrfunc tp_setattr; PyAsyncMethods *tp_as_async; reprfunc tp_repr; PyNumberMethods *tp_as_number; PySequenceMethods *tp_as_sequence; PyMappingMethods *tp_as_mapping; hashfunc tp_hash; ternaryfunc tp_call; reprfunc tp_str; getattrofunc tp_getattro; setattrofunc tp_setattro; PyBufferProcs *tp_as_buffer; unsigned long tp_flags; const char *tp_doc; traverseproc tp_traverse; inquiry tp_clear; richcmpfunc tp_richcompare; Py_ssize_t tp_weaklistoffset; getiterfunc tp_iter; iternextfunc tp_iternext; PyMethodDef *tp_methods; PyMemberDef *tp_members; PyGetSetDef *tp_getset; PyTypeObject *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; inquiry tp_is_gc; PyObject *tp_bases; PyObject *tp_mro; PyObject *tp_cache; void *tp_subclasses; PyObject *tp_weaklist; destructor tp_del; unsigned int tp_version_tag; destructor tp_finalize; vectorcallfunc tp_vectorcall; unsigned char tp_watched;};
類型對象在底層對應(yīng)的是 struct _typeobject,或者說 PyTypeObject,它保存了實(shí)例對象的元信息。
所以不難發(fā)現(xiàn),Python 中實(shí)例對象在底層會對應(yīng)不同的結(jié)構(gòu)體實(shí)例,但類型對象則是對應(yīng)同一個結(jié)構(gòu)體實(shí)例。換句話說無論是 int、str、dict 等內(nèi)置類型,還是我們使用 class 關(guān)鍵字自定義的類型,它們在 C 的層面都是由 PyTypeObject 這個結(jié)構(gòu)體實(shí)例化得到的,只不過內(nèi)部字段的值不同,PyTypeObject 這個結(jié)構(gòu)體在實(shí)例化之后得到的類型對象也不同。
然后我們來看看 PyTypeObject 里面的字段都代表啥含義,字段還是比較多的,我們逐一介紹。
宏,會被替換為 PyVarObject,所以類型對象是一個變長對象。因此類型對象也有引用計(jì)數(shù)和類型,這與我們前面分析的是一致的。
對應(yīng) Python 中類型對象的 __name__ 屬性,即類型對象的名稱。
# 類型對象在底層對應(yīng)的是 PyTypeObject 結(jié)構(gòu)體實(shí)例# 它的 tp_name 字段表示類型對象的名稱print(int.__name__) # int# 動態(tài)創(chuàng)建一個類A = type("我是 A", (object,), {})print(A.__name__) # 我是 A
所以任何一個類型對象都有 __name__ 屬性,也就是都有名稱。
析構(gòu)函數(shù),對應(yīng) Python 中類型對象的 __del__,會在實(shí)例對象被銷毀時執(zhí)行。
如果想調(diào)用一個對象,那么它的類型對象要定義 __call__ 函數(shù)。
class A: def __call__(self, *args, **kwargs): return "被調(diào)用了"a = A()# 如果調(diào)用 a,那么 type(a) 要定義 __call__ 函數(shù)print(a())"""被調(diào)用了"""# 底層會轉(zhuǎn)成如下邏輯print(A.__call__(a))"""被調(diào)用了"""# 函數(shù)也是一個實(shí)例對象,它能被調(diào)用# 說明 type(函數(shù)) 也一定實(shí)現(xiàn)了 __call__def some_func(name, age): return f"name: {name}, age: {age}"# 函數(shù)的類型是 functionprint(type(some_func))"""<class 'function'>"""# 調(diào)用函數(shù)print(some_func("古明地覺", 17))"""name: 古明地覺, age: 17"""# 也可以這么做print(type(some_func).__call__(some_func, "古明地覺", 17))"""name: 古明地覺, age: 17"""
以上就是對象最通用的調(diào)用邏輯,但通用也意味著平庸,這種調(diào)用方式的性能是不高的。自定義類的實(shí)例對象還好,因?yàn)樾枰С终{(diào)用的場景不多,而函數(shù)則不同,盡管它也是實(shí)例對象,但它生下來就是要被調(diào)用的。
如果函數(shù)調(diào)用也走通用邏輯的話,那么效率不高,因此 Python 從 3.8 開始引入了 vectorcall 協(xié)議,即矢量調(diào)用協(xié)議,用于優(yōu)化和加速函數(shù)調(diào)用。至于它是怎么優(yōu)化的,后續(xù)剖析函數(shù)的時候再細(xì)說。
總之當(dāng)一個對象被調(diào)用時,如果它支持 vectorcall 協(xié)議,那么會通過 tp_vectorcall_offset 找到實(shí)現(xiàn)矢量調(diào)用的函數(shù)指針。
注意:vectorcall 函數(shù)指針定義在實(shí)例對象中,所以 tp_vectorcall_offset 字段維護(hù)了 vectorcall 函數(shù)指針在實(shí)例對象中的偏移量,該偏移量用于定位到一個特定的函數(shù)指針,這個函數(shù)指針符合 vectorcall 協(xié)議。
如果類型對象的 tp_vectorcall_offset 為 0,表示其實(shí)例對象不支持矢量調(diào)用,因此會退化為常規(guī)調(diào)用,即通過 __call__ 進(jìn)行調(diào)用。
對應(yīng) Python 中類型對象的 __getattr__ 和 __setattr__,用于操作實(shí)例對象的屬性。但這兩個字段已經(jīng)不推薦使用了,因?yàn)樗笤诓僮鲗傩詴r,屬性名必須為 C 字符串,以及不支持通過描述符協(xié)議處理屬性。
所以這兩個字段主要用于兼容舊版本,現(xiàn)在應(yīng)該使用 tp_getattro 和 tp_setattro。
tp_as_number:實(shí)例對象為數(shù)值時,所支持的操作。這是一個結(jié)構(gòu)體指針,指向的結(jié)構(gòu)體中的每一個字段都是一個函數(shù)指針,指向的函數(shù)就是對象可以執(zhí)行的操作,比如四則運(yùn)算、左移、右移、取模等等。
tp_as_sequence:實(shí)例對象為序列時,所支持的操作,也是一個結(jié)構(gòu)體指針。
tp_as_mapping:實(shí)例對象為映射時,所支持的操作,也是一個結(jié)構(gòu)體指針。
tp_as_async:實(shí)例對象為協(xié)程時,所支持的操作,也是一個結(jié)構(gòu)體指針。
對應(yīng) Python 中類型對象的 __repr__ 和 __str__,用于控制實(shí)例對象的打印輸出。
對應(yīng) Python 中類型對象的 __hash__,用于定義實(shí)例對象的哈希值。
對應(yīng) Python 中類型對象的 __call__,用于控制實(shí)例對象的調(diào)用行為。當(dāng)然這屬于常規(guī)調(diào)用,而對象不僅可以支持常規(guī)調(diào)用,還可以支持上面提到的矢量調(diào)用(通過減少參數(shù)傳遞的開銷,提升調(diào)用性能)。
但要注意的是,不管使用哪種調(diào)用協(xié)議,對象調(diào)用的行為必須都是相同的。因此一個對象如果支持矢量調(diào)用,那么它也必須支持常規(guī)調(diào)用,換句話說對象如果實(shí)現(xiàn)了 vectorcall,那么它的類型對象也必須實(shí)現(xiàn) tp_call。
如果你在實(shí)現(xiàn) vectorcall 之后發(fā)現(xiàn)它比 tp_call 還慢,那么你就不應(yīng)該實(shí)現(xiàn) vectorcall,因?yàn)閷?shí)現(xiàn) vectorcall 是有條件的,當(dāng)條件不滿足時性能反而會變差。
對應(yīng) Python 中類型對象的 __getattr__ 和 __setattr__。
指向 PyBufferProcs 類型的結(jié)構(gòu)體,用于共享內(nèi)存。通過暴露出一個緩沖區(qū),可以和其它對象共享同一份數(shù)據(jù),因此當(dāng)類型對象實(shí)現(xiàn)了 tp_as_buffer,我們也說其實(shí)例對象實(shí)現(xiàn)了緩沖區(qū)協(xié)議,舉個例子。
import numpy as npbuf = bytearray(b"abc")# 和 buf 共享內(nèi)存arr = np.frombuffer(buf, dtype="uint8")print(arr) # [97 98 99]# 修改 bufbuf[0] = 255# 會發(fā)現(xiàn) arr 也改變了,因?yàn)樗?buf 共用一塊內(nèi)存print(arr) # [255 98 99]
所以 tp_as_buffer 主要用于那些自身包含大量數(shù)據(jù),且需要允許其它對象直接訪問的類型。通過實(shí)現(xiàn)緩沖區(qū)協(xié)議,其它對象可以直接共享數(shù)據(jù),而無需事先拷貝,這在處理大型數(shù)據(jù)或進(jìn)行高性能計(jì)算時非常有用。
關(guān)于緩沖區(qū)協(xié)議,后續(xù)還會詳細(xì)介紹。
對應(yīng) Python 中類型對象的 __flags__,負(fù)責(zé)提供類型對象本身的附加信息,通過和指定的一系列標(biāo)志位進(jìn)行按位與運(yùn)算,即可判斷該類型是否具有某個特征。
那么標(biāo)志位都有哪些呢?我們介紹幾個常見的。
// Include/object.h// 類型對象的內(nèi)存是否是動態(tài)分配的// 像內(nèi)置的類型對象屬于靜態(tài)類,它們不是動態(tài)分配的#define Py_TPFLAGS_HEAPTYPE (1UL << 9)// 類型對象是否允許被繼承#define Py_TPFLAGS_BASETYPE (1UL << 10)// 類型對象的實(shí)例對象是否參與垃圾回收#define Py_TPFLAGS_HAVE_GC (1UL << 14)// 類型對象是否是抽象基類#define Py_TPFLAGS_IS_ABSTRACT (1UL << 20)
我們通過 Python 來演示一下。
# 是否是自定義的動態(tài)類Py_TPFLAGS_HEAPTYPE = 1 << 9class A: pass# 如果與運(yùn)算的結(jié)果為真,則表示是動態(tài)類,否則不是print(A.__flags__ & Py_TPFLAGS_HEAPTYPE)"""512"""print(int.__flags__ & Py_TPFLAGS_HEAPTYPE)"""0"""# 類型對象是否允許被繼承Py_TPFLAGS_BASETYPE = 1 << 10# object 顯然允許被繼承,因此與運(yùn)算的結(jié)果為真print(object.__flags__ & Py_TPFLAGS_BASETYPE)"""1024"""# 但 memoryview 就不允許被繼承try: class B(memoryview): passexcept TypeError as e: print(e) """ type 'memoryview' is not an acceptable base type """print(memoryview.__flags__ & Py_TPFLAGS_BASETYPE)"""0"""# 類型對象的實(shí)例對象是否參與垃圾回收Py_TPFLAGS_HAVE_GC = 1 << 14# int 的實(shí)例對象(整數(shù))不會產(chǎn)生循環(huán)引用,所以不會參與垃圾回收print(int.__flags__ & Py_TPFLAGS_HAVE_GC)"""0"""# 但列表是會參與垃圾回收的print(list.__flags__ & Py_TPFLAGS_HAVE_GC)"""16384"""# 類型對象是否是抽象基類Py_TPFLAGS_IS_ABSTRACT = 1 << 20from abc import ABCMeta, abstractmethod# 顯然 C 不是抽象基類,而 D 是class C: passclass D(metaclass=ABCMeta): @abstractmethod def foo(self): passprint(C.__flags__ & Py_TPFLAGS_IS_ABSTRACT)"""0"""print(D.__flags__ & Py_TPFLAGS_IS_ABSTRACT)"""1048576"""
所以這就是 tp_flags 的作用,它負(fù)責(zé)描述一個類型對象都具有哪些額外特征。
對應(yīng) Python 中類型對象的 __doc__。
class People: """以前我沒得選"""print(People.__doc__)"""以前我沒得選"""
這個比較簡單。
這兩個字段是一對,負(fù)責(zé)參與垃圾回收機(jī)制。
負(fù)責(zé)實(shí)現(xiàn)對象的比較邏輯,包含 >、>=、<、<=、!=、==。
弱引用列表的偏移量。
對應(yīng) Python 中類型對象的 __iter__ 和 __next__。
負(fù)責(zé)保存類型對象里的成員函數(shù),我們以 list 為例。
負(fù)責(zé)指定可以綁定在實(shí)例對象上的屬性,我們使用 class 關(guān)鍵字定義動態(tài)類的時候,會在 __init__ 函數(shù)中給實(shí)例對象綁定屬性,而對于底層 C 來說,需要通過 tp_members 字段。
以 slice 為例,它負(fù)責(zé)創(chuàng)建一個切片。
lst = list(range(10))print(lst[1: 7: 2]) # [1, 3, 5]# 等價于print(lst[slice(1, 7, 2)]) # [1, 3, 5]
slice 是一個底層實(shí)現(xiàn)好的靜態(tài)類,接收 start、end、step 三個參數(shù),所以它底層的 tp_members 就是這么定義的。
對于靜態(tài)類而言,可以給 self 綁定哪些屬性、以及類型是什么,都已經(jīng)事先在 tp_members 里面寫死了,后續(xù)不可以新增或刪除屬性。
s = slice(1, 7, 2)# 靜態(tài)類的實(shí)例對象不可以新增或刪除屬性try: s.xx = "xx"except AttributeError as e: print(e) """ 'slice' object has no attribute 'xx' """# 至于能否修改,則看定義屬性時是否要求屬性是 READONLY# 對于 slice 來說,它的三個屬性都是 READONLY,所以不能修改try: s.start = 2except AttributeError as e: print(e) """ readonly attribute """
但使用 class 自定義的動態(tài)類而言,新增、刪除、修改屬性都是可以的,至于里面的更多細(xì)節(jié),后續(xù)在介紹類的時候會詳細(xì)剖析。
指向一個 PyGetSetDef 結(jié)構(gòu)體數(shù)組,里面的每個結(jié)構(gòu)體都定義了一個屬性的名稱、獲取該屬性的函數(shù)、設(shè)置該屬性的函數(shù)、屬性的文檔字符串。
所以我們發(fā)現(xiàn) tp_getset 的作用不就類似于 @property 裝飾器嗎?tp_getset 數(shù)組里面的每個結(jié)構(gòu)體負(fù)責(zé)實(shí)現(xiàn)一個 property 屬性。
對應(yīng) Python 中類型對象的 __base__,返回繼承的第一個父類。
對應(yīng) Python 中類型對象的 __dict__,即屬性字典。
對應(yīng) Python 中類型對象的 __get__ 和 __set__,用于實(shí)現(xiàn)描述符。
注意它和 tp_dict 的區(qū)別,tp_dict 表示類型對象的屬性字典,而 tp_dictoffset 表示實(shí)例對象的屬性字典在實(shí)例對象中的偏移量。關(guān)于屬性字典,后續(xù)會詳細(xì)介紹。
對應(yīng) Python 中類型對象的 __init__,用于實(shí)例對象屬性的初始化。
負(fù)責(zé)為實(shí)例對象申請內(nèi)存,申請多大呢?取決于 tb_basicsize 和 tp_itemsize。
對應(yīng) Python 中類型對象的 __new__,即構(gòu)造函數(shù),在 tp_new 內(nèi)部會調(diào)用 tp_alloc 為實(shí)例對象申請內(nèi)存。
內(nèi)存釋放函數(shù),負(fù)責(zé)釋放實(shí)例對象所占的內(nèi)存,注意它和 tp_dealloc 的區(qū)別與聯(lián)系。tp_dealloc 表示析構(gòu)函數(shù),當(dāng)對象的引用計(jì)數(shù)降到零的時候執(zhí)行,內(nèi)部會負(fù)責(zé)如下工作。
所以要注意這幾個字段之間的區(qū)別,我們再總結(jié)一下。
指示該類型對象的實(shí)例對象是否參與垃圾回收。
如果參與垃圾回收,那么 tp_flags & Py_TPFLAGS_HAVE_GC 的結(jié)果不等于 0。
對應(yīng) Python 中類型對象的 __bases__,返回一個元組,里面包含直接繼承的所有父類。
對應(yīng) Python 中類型對象的 __mro__,返回一個元組,里面包含自身以及直接繼承和間接繼承的所有父類,直到 object。
注意:返回的元組中的類是有順序關(guān)系的,它基于 C3 線性化算法生成,定義了方法解析的順序。當(dāng) Python 需要查找方法或?qū)傩詴r,將按照此順序進(jìn)行搜索。
該字段不再使用,因此這里不做介紹。
等價于 Python 中類型對象的 __subclasses__,會返回繼承該類的所有子類。
class A: passclass B(A): passclass C(B): passprint(A.__subclasses__())"""[<class '__main__.B'>]"""
但是只返回直接繼承的子類,間接繼承的不算,比如這里只返回了 B,而 C 沒有返回。
實(shí)例對象的弱引用列表,注意:每個實(shí)例對象都會有,而前面還提到了一個 tp_weaklistoffset,它便是弱引用列表在實(shí)例對象當(dāng)中的偏移量。如果偏移量為 0,那么表示當(dāng)前類型對象的實(shí)例對象不支持弱引用。
和 tp_dealloc 作用相同,但 tp_del 主要是兼容以前的舊版本,現(xiàn)在直接使用 tp_dealloc 即可。
用于標(biāo)記類型對象的版本,每當(dāng)類型的定義發(fā)生變化時(例如添加、刪除或修改成員函數(shù)),這個版本標(biāo)簽就會更新。解釋器會使用這個版本標(biāo)簽來確定方法緩存是否有效,從而避免在每次方法調(diào)用時都重新解析和查找。
負(fù)責(zé)在對象被銷毀之前執(zhí)行相應(yīng)的清理操作,確保資源得到妥善處理,它的調(diào)用時機(jī)在對象的引用計(jì)數(shù)達(dá)到零之后、tp_dealloc(負(fù)責(zé)釋放對象的內(nèi)存)被調(diào)用之前。
該字段不常用,一般只出現(xiàn)在生成器和協(xié)程當(dāng)中。然后 tp_dealloc、tp_del、tp_finalize 三個字段的類型是一致的,都是 destructor 類型,那么它們?nèi)哂惺裁磪^(qū)別呢?
以上就是 PyTypeObject 的各個字段的含義。
下面來介紹一些常見的類型在底層的定義。
Python 底層的 C API 和對象的命名都遵循統(tǒng)一的標(biāo)準(zhǔn),比如類型對象均以 Py***_Type 的形式命名,當(dāng)然啦,它們都是 PyTypeObject 結(jié)構(gòu)體實(shí)例。
所以我們發(fā)現(xiàn),Python 里的類在底層是以全局變量的形式靜態(tài)定義好的。
所以實(shí)例對象可以有很多個,但類型對象則是唯一的,在底層直接以全局變量的形式靜態(tài)定義好了。
比如列表的類型是 list,列表可以有很多個,但 list 類型對象則全局唯一。
data1 = [1, 2, 3]data2 = [4, 5, 6]print( data1.__class__ is data2.__class__ is list) # True
如果站在 C 的角度來理解的話:
data1 和 data2 變量均指向了列表,列表在底層對應(yīng) PyListObject 結(jié)構(gòu)體實(shí)例。里面字段的含義之前說過,但需要注意的是,指針數(shù)組里面保存的是對象的指針,而不是對象。不過為了方便,圖中就用對象代替了。
然后列表的類型是 list,在底層對應(yīng) PyList_Type,它是 PyTypeObject 結(jié)構(gòu)體實(shí)例,保存了列表的元信息(比如內(nèi)存分配信息、支持的相關(guān)操作等)。
而將這兩者關(guān)聯(lián)起來的便是 ob_type,它位于 PyObject 中,是所有對象都具有的。因?yàn)樽兞恐皇且粋€ PyObject * 指針,那么解釋器要如何得知變量指向的對象的類型呢?答案便是通過 ob_type 字段。
類型對象全局唯一,在底層以全局變量的形式存在,不管是什么類型對象,均由 PyTypeObject 結(jié)構(gòu)體實(shí)例化得到,而不同的實(shí)例對象則對應(yīng)不同的結(jié)構(gòu)體。
將實(shí)例對象和類型對象關(guān)聯(lián)起來的,則是實(shí)例對象的 ob_type 字段,在 Python 里面可以通過調(diào)用 type 或者獲取 __class__ 屬性查看。
關(guān)于類型對象的更多內(nèi)容,后續(xù)會繼續(xù)介紹。
本文鏈接:http://www.tebozhan.com/showinfo-26-88309-0.html詳解 PyTypeObject,Python 類型對象的載體
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com