Python虚拟机类机制之descriptor(三)
從slot到descriptor
在Python虛擬機類機制之填充tp_dict(二)這一章的末尾,我們介紹了slot,slot包含了很多關于一個操作的信息,但是很可惜,在tp_dict中,與__getitem__關聯在一起的,一定不會是一個slot,原因很簡單,slot不是一個PyObject,它不能存放在dict對象中。當然,我們再深入思考一下,會發現slot也不會被“調用”。既然slot不是一個PyObject,那么它就沒有type,也就無從談起什么tp_call了,所以slot是無論如何也不滿足前面所描述的Python的“可調用”這個概念
前面我們說過,Python虛擬機會在tp_dict找到__geiitem__對應的操作后,調用該操作,所以在tp_dict中與__getitem__對應的只能是另一個包裝了slot的PyObject,在Python中,我們稱為descriptor
在Python內部,存在多種descriptor,與PyTypeObject中的操作對應的是PyWrapperDescrObject。在此后的描述,我們將用術語descriptor來專門表示PyWrapperDescrObject。一個descriptor包含一個slot,其創建是通過PyDescr_NewWrapper
descrobject.h?
#define PyDescr_COMMON \PyObject_HEAD \PyTypeObject *d_type; \PyObject *d_nametypedef struct {PyDescr_COMMON;struct wrapperbase *d_base;void *d_wrapped; /* This can be any function pointer */ } PyWrapperDescrObject;
descrobject.c
static PyDescrObject * descr_new(PyTypeObject *descrtype, PyTypeObject *type, const char *name) {PyDescrObject *descr;//申請空間descr = (PyDescrObject *)PyType_GenericAlloc(descrtype, 0);if (descr != NULL) {Py_XINCREF(type);descr->d_type = type;descr->d_name = PyString_InternFromString(name);if (descr->d_name == NULL) {Py_DECREF(descr);descr = NULL;}}return descr; }PyObject * PyDescr_NewWrapper(PyTypeObject *type, struct wrapperbase *base, void *wrapped) {PyWrapperDescrObject *descr;descr = (PyWrapperDescrObject *)descr_new(&PyWrapperDescr_Type,type, base->name);if (descr != NULL) {descr->d_base = base;descr->d_wrapped = wrapped;}return (PyObject *)descr; }
Python內部的各種descriptor都將包含PyDescr_COMMON,其中的d_type被設置為PyDescr_NewWrapper的參數type,而d_wrapped則存放著最重要的信息:操作對應的函數指針,比如對于PyList_Type來說,其tp_dict["__getitem__"].d_wrapped就是&mp_subscript。而slot則被存放在了d_base中
PyWrapperDescrObject的type是PyWrapperDescr_Type,其中的tp_call是wrapperdescr_call,當Python虛擬機調用一個descriptor時,也就會調用wrapperdescr_call,對于descriptor的調用過程,后面還會詳細解析
建立聯系
排序后的結果仍然存放在slotdefs中,Python虛擬機就可以從頭到尾遍歷slotdefs,基于每一個slot建立一個descriptor,然后在tp_dict中建立從操作名到descriptor的關聯,這個過程在add_operators中完成
typeobject.c
static int add_operators(PyTypeObject *type) {PyObject *dict = type->tp_dict;slotdef *p;PyObject *descr;void **ptr;//對slotdefs進行排序init_slotdefs();for (p = slotdefs; p->name; p++) {//如果slot中沒有指定wrapper,則不處理if (p->wrapper == NULL)continue;//獲得slot對應的操作在PyTypeObject中的函數指針ptr = slotptr(type, p->offset);if (!ptr || !*ptr)continue;//如果tp_dict中存在操作名,則放棄if (PyDict_GetItem(dict, p->name_strobj))continue;//創建descriptordescr = PyDescr_NewWrapper(type, p, *ptr);if (descr == NULL)return -1;//將(操作名,descriptor)放入tp_dict中if (PyDict_SetItem(dict, p->name_strobj, descr) < 0)return -1;Py_DECREF(descr);}if (type->tp_new != NULL) {if (add_tp_new_wrapper(type) < 0)return -1;}return 0; }
在add_operators中,首先會調用前面剖析過的init_slotdefs函數進行排序,然后遍歷排序完后的slotdefs結構體數組,對其中每一個slot(slotdef),通過slotptr獲得該slot對應的操作在PyTypeObject中的函數指針,并接著創建descriptor,在tp_dict中建立從操作名(slotdef.name_strobj)到操作(descriptor)的關聯
需要注意的是,在創建descriptor之前,Python虛擬機會檢查在tp_dict中操作名是否已存在,如果已經存在,則不會再次建立從操作名到操作的關聯。正是這種檢查機制與上面的排序機制相結合,使得Python虛擬機能夠在擁有相同操作名的多個操作中選擇優先級最高的操作
在add_operators中,上面描述的動作都很直觀、簡單。而最難的動作隱藏在slotptr這個函數中,它的功能是完成slot到slot對應操作的真實函數指針的轉換。我們已經知道,在slot中存放著操作的offset,但很不幸,這個offset是相對于PyHeadTypeObject的偏移,而操作的真實函數指針則在PyTypeObject中指定。更不幸的是,PyTypeObject和PyHeadTypeObject不是同構的,因為PyHeadTypeObject中包含了PyNumberMethods結構體,而PyTypeObject中只包含了PyNumberMethods*指針。所以slot中存儲的這個關于操作的offset對于PyTypeObject來說,不可能直接使用,必須通過轉換
舉個例子,假如說調用slotptr(&PyList_Type, offset(PyHeadTypeObject, mp_subscript)),首先判斷這個偏移大于offset(PyHeadTypeObject, as_mapping),所以會先從PyTypeObject對象中獲得as_mapping指針P,然后在P的基礎上進行偏移就可以得到實際的函數地址了,而偏移量delta為:
delta = offset(PyHeadTypeObject, mp_subscript) - offset(PyHeadTypeObject, as_mapping)
這個復雜的轉換過程在slotptr中完成:
typeobject.c
static void ** slotptr(PyTypeObject *type, int ioffset) {char *ptr;long offset = ioffset;assert(offset >= 0);assert((size_t)offset < offsetof(PyHeapTypeObject, as_buffer));//判斷從PyHeapTypeObject中排后面的PySequenceMethods開始if ((size_t)offset >= offsetof(PyHeapTypeObject, as_sequence)) {ptr = (char *)type->tp_as_sequence;offset -= offsetof(PyHeapTypeObject, as_sequence);}else if ((size_t)offset >= offsetof(PyHeapTypeObject, as_mapping)) {ptr = (char *)type->tp_as_mapping;offset -= offsetof(PyHeapTypeObject, as_mapping);}else if ((size_t)offset >= offsetof(PyHeapTypeObject, as_number)) {ptr = (char *)type->tp_as_number;offset -= offsetof(PyHeapTypeObject, as_number);}else {ptr = (char *)type;}if (ptr != NULL)ptr += offset;return (void **)ptr; }
為什么判斷首先從PySequenceMethods開始,然后向前,依次判斷PyMappingMethods和PyNumberMethods呢?假如我們先從PyNumberMethods開始判斷,如果一個操作的offset大于PyHeadTypeObject中as_number在PyNumberMethods的偏移量,那么我們還是沒有辦法確定在這個操作是屬于PyNumberMethods還是屬于PyMappingMethods或PySequenceMethods。只有從后往前進行判斷,才能解決這個問題
現在,我們摸清楚Python在改造PyTypeObject時對tp_dict做了什么,圖1-1顯示了PyList_Type完成初始化之后的整個布局,其中包括我們討論的descriptor和slot
圖1-1? ?add_operators完成之后的PyList_Type
在圖1-1中,PyList_Type.tp_as_mapping中延伸出去的部分是在編譯時已經確定好的,而從tp_dict中延伸出去的部分是在Python運行時環境初始化才建立的。
PyType_Ready在通過add_operators添加了PyTypeObject對象中定義了的一些操作后,還會通過add_methods、add_members、add_getset添加在PyTypeObject中定義的tp_methods、tp_members和tp_getset函數集,這些過程與add_operators類似,不過最后添加到tp_dict中的descriptor就不再是PyWrapperDescrObject,而分別是PyMethodDescrObject、PyMemberDescrObject、PyGetSetDescrObject
圖1-1所顯示的class對象大部分正確,但還不算全部正確,考慮下面的例子:?
>>> class A(list): ... def __repr__(self): ... return "Python" ... >>> s = "%s" % A() >>> s 'Python'
熟悉Python的人都知道,__repr__是Python中的特殊方法。當Python執行表達式"s = '%s' %A()"時,最終會調用A.tp_repr。如果按照圖1-1的布局,并且對照PyList_Type,那么就應該調用list_repr這個函數,但并不是這樣的,Python虛擬機最終調用的是A中重寫后的__repr__。這意味著,Python在初始化A時,對tp_repr進行了特殊處理。為什么Python虛擬機會知道要對tp_repr進行特殊處理呢?答案還是在slot身上
在slotdefs中,有一條slot為TPSLOT:
typeobject.c
TPSLOT("__repr__", tp_repr, slot_tp_repr, wrap_unaryfunc, "x.__repr__() <==> repr(x)")
Python虛擬機在初始化A時,會檢查<class A>的tp_dict中是否存在__repr__。在后面剖析用戶自定義的class對象時,我們會看到,因為在定義class A時重寫__repr__這個操作,所以A.tp_dict中__repr__一開始就會存在,Python虛擬機會檢測到它的存在。一旦檢測到__repr__存在,Python虛擬機將tp_repr這個函數指針替換為slot中指定的&slot_tp_repr。所以當Python虛擬機調用A.tp_repr時,實際上執行的是slot_tp_repr
typeobject.c
static PyObject * slot_tp_repr(PyObject *self) {PyObject *func, *res;static PyObject *repr_str;//[1]:查找__repr__屬性func = lookup_method(self, "__repr__", &repr_str);if (func != NULL) {//[2]:調用__repr__對應的對象res = PyEval_CallObject(func, NULL);Py_DECREF(func);return res;}PyErr_Clear();return PyString_FromFormat("<%s object at %p>",self->ob_type->tp_name, self); }
在slot_tp_repr中,會尋找__repr__屬性對應的對象,正好就會找到我們在A中重寫的函數,這個對象其實是一個PyFunctionObject。這樣一來,就完成了對默認list的repr行為的替換,所以對A來說,其初始化結束后的內存布局則如圖1-2所示:
圖1-2? ?初始化完成后的A
?
當然,并是不會A中所有的操作都會有這樣的變化。A的其他操作還是會指向PyList_Type中指定的函數,比如tp_iter還是會指向list_iter。對于A來說,這個變化是在fixup_slot_dispatchers(PyTypeObject* type)中完成的,對于內置class對象,不會進行這樣的操作,這個操作是屬于創建自定義class對象時的動作
對于A來說,這個變化是在fixup_slot_dispatchers(PyTypeObject* type)中完成的,對于內置class對象,不會進行這樣的操作,這個操作是屬于創建自定義class對象時的動作
確定MRO
所謂的MRO,即是指Method Resolve Order,更一般地,也是一個class對象的屬性解析順序。如果Python像java那樣僅支持單繼承,那就不是一個問題了。但是Python是支持多繼承的,在多重繼承時,就必須設置按照何種順序解析屬性,考慮如下Python代碼:
>>> class A(list): ... def show(self): ... print("A::show") ... >>> class B(list): ... def show(self): ... print("B::show") ... >>> class C(A): ... pass ... >>> class D(C, B): ... pass ... >>> d = D() >>> d.show() A::show
由于D的基類A和B中都實現了show,那么在調用d.show()時,究竟是調用A的show方法還是B的show方法呢?Python內部在PyType_Ready中通過mro_internal函數完成了對一個類型的mro順序的建立。Python虛擬機將創建一個tupple對象,在對象中依次存放著一組class對象。在tupple中,class對象的順序就是Python虛擬機在解析屬性時的mro順序。最終這個tupple將被保存在PyTypeObject.tp_mro中
對于上述的class D,Python虛擬機會在內部創建一個list,其中根據D的聲明依次放入D和它的基類,如圖1-3所示:
圖1-3? ?D建立mro列表時Python虛擬機內部的輔助list
?注意在list的最后一項存放著一個包含所有D的直接基類列表。Python虛擬機將從左到右遍歷該list,當訪問到list中的任一個基類時,如果基類存在mro列表,則會轉而訪問基類的mro列表。在訪問的過程中,不斷將所訪問到的class對象放入到D自身的mro列表中
我們跟蹤這個遍歷的過程來看一下:
當遍歷的過程結束后,D的mro列表也就存儲了一個class對象的順序列表了。從上面的遍歷過程可以看到,這個列表是(D、C、A、B、list、object),我們可以來驗證一下:
>>> for t in D.__mro__: ... print(t) ... <class '__main__.D'> <class '__main__.C'> <class '__main__.A'> <class '__main__.B'> <class 'list'> <class 'object'>
圖1-4? ?展示不同順序下mro列表
繼承基類操作
Python虛擬機確定了mro列表后,就會遍歷mro列表(注意,由于第一個class對象的mro列表的第一項總是其自身,所以遍歷是從第二項開始的)。在mro列表中實際上還存儲的就是class對象的所有直接和間接基類,Python虛擬機會將class對象自身沒有設置而基類中設置了的操作拷貝到class對象中,從而完成對基類操作的繼承動作:
這個繼承操作的動作發生在inherit_slots中
typeobject.c
int PyType_Ready(PyTypeObject *type) {……bases = type->tp_mro;n = PyTuple_GET_SIZE(bases);for (i = 1; i < n; i++) {PyObject *b = PyTuple_GET_ITEM(bases, i);if (PyType_Check(b))inherit_slots(type, (PyTypeObject *)b);}…… }
在inherit_slots中,會拷貝相當多的操作,這里我們拿nb_add來做個例子:
typeobject.c
static void inherit_slots(PyTypeObject *type, PyTypeObject *base) {PyTypeObject *basebase;#define SLOTDEFINED(SLOT) \(base->SLOT != 0 && \(basebase == NULL || base->SLOT != basebase->SLOT))#define COPYSLOT(SLOT) \if (!type->SLOT && SLOTDEFINED(SLOT)) type->SLOT = base->SLOT#define COPYNUM(SLOT) COPYSLOT(tp_as_number->SLOT)if (type->tp_as_number != NULL && base->tp_as_number != NULL) {basebase = base->tp_base;if (basebase->tp_as_number == NULL)basebase = NULL;COPYNUM(nb_add);……}…… }
我們知道PyBool_Type中并沒有設置nb_add操作,但它的tp_base設置的是&PyInt_Type,而PyInt_Type中卻設置了nb_add操作。所以我們可以在PyType_Ready中添加輸出語句,當處理type分別為bool和int時,輸出其nb_add的地址,進行驗證。因為按照inherit_slots的結果,這兩個地址應該都指向同一個地址,即int_add的地址
typeobject.c
int PyType_Ready(PyTypeObject *type) {……for (i = 1; i < n; i++) {PyObject *b = PyTuple_GET_ITEM(bases, i);if (PyType_Check(b))inherit_slots(type, (PyTypeObject *)b);}//打印bool中的nb_add地址if (strcmp(type->tp_name, "bool") == 0) {printf("bool nb_add: 0x%X\n", *(type->tp_as_number->nb_add));}//打印int中的nb_add地址if (strcmp(type->tp_name, "int") == 0) {printf("int nb_add: 0x%X\n", *(type->tp_as_number->nb_add));}…… }
然后打開Python命令行,可以看到int類型和bool類型在初始化時打印其nb_add地址:
# ./python int nb_add: 0x43C570 bool nb_add: 0x43C570
這個結果預示著Python中的兩個bool對象,我們可以進行加法操作
填充基類中的子類列表
到這里,PyType_Ready還剩下最后一個重要的動作了:設置基類中的子類列表。在每一個PyTypeObject中,有一個tp_subclasses,這個東西在PyType_Type完成后將是一個list對象。其中存放著所有直接繼承該類型的class對象。PyType_Ready通過調用add_subclass完成這個向tp_subclass中填充子類對象的動作
typeobject.c
int PyType_Ready(PyTypeObject *type) {PyObject *dict, *bases;PyTypeObject *base;Py_ssize_t i, n;……bases = type->tp_bases;……n = PyTuple_GET_SIZE(bases);for (i = 0; i < n; i++) {PyObject *b = PyTuple_GET_ITEM(bases, i);if (PyType_Check(b) &&add_subclass((PyTypeObject *)b, type) < 0)goto error;}…… }
我們驗證這個子類列表的存在:?
>>> int.__subclasses__() [<class 'bool'>] >>> object.__subclasses__() [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>,…… >>>
果然,object是萬物之母,很多的類都直接繼承于object。可以看到,Python虛擬機對Python的內置類型對應的PyTypeObject進行了多種復雜的改造工作,總結一下,主要包括:
- 設置type信息,基類及基類列表
- 填充tp_dict
- 確定mro列表
- 基于mro列表從基類繼承操作
- 設置基類的子類列表
?
轉載于:https://www.cnblogs.com/beiluowuzheng/p/9621918.html
總結
以上是生活随笔為你收集整理的Python虚拟机类机制之descriptor(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [SCOI2007] 蜥蜴 (最大流)
- 下一篇: Httpd之检测与安装