填充tp_dicthtml
在Python虛擬機類機制之對象模型(一)這一章中,咱們介紹了Python的內置類型type若是要完成到class對象的轉變,有一個重要的步驟就是填充tp_dict對象,這是一個極其繁雜的過程python
typeobject.c數組
int PyType_Ready(PyTypeObject *type) { PyObject *dict, *bases; PyTypeObject *base; Py_ssize_t i, n; …… //設定tp_dict dict = type->tp_dict; if (dict == NULL) { dict = PyDict_New(); if (dict == NULL) goto error; type->tp_dict = dict; } //將與type相關的descriptor加入到tp_dict中 if (add_operators(type) < 0) goto error; if (type->tp_methods != NULL) { if (add_methods(type, type->tp_methods) < 0) goto error; } if (type->tp_members != NULL) { if (add_members(type, type->tp_members) < 0) goto error; } if (type->tp_getset != NULL) { if (add_getset(type, type->tp_getset) < 0) goto error; } …… }
在這個階段,將完成("__add__", &nb_add)在tp_dict的映射。這個階段的add_operators、add_methods、add_members、add_getset都是這樣完成填充tp_dict的動做。那麼,一個問題浮現了,Python虛擬機是如何知道"__add__"和nb_add之間存在關聯呢?這種關聯是在Python源代碼中預先約定好的,存放在一個名爲slotdefs的全局數組app
slot與操做排序函數
在進入填充tp_dict的複雜操做以前,咱們先來介紹Python內部的一個概念:slot。在Python內部,slot能夠視爲表示PyTypeObject中定義的操做,一個操做對應一個slot,可是slot不單單包含一個函數指針,它還包含其餘一些信息。在Python內部,slot是經過slotdef這個結構體來實現的post
//typeobject.c typedef struct wrapperbase slotdef; //descrobject.h struct wrapperbase { char *name; int offset; void *function; wrapperfunc wrapper; char *doc; int flags; PyObject *name_strobj; };
在一個slot中,存儲着與PyTypeObject中一種操做相對應的各類信息。name就是操做對應的名稱,如字符串__add__,offset則是操做的函數地址在PyHeapTypeObject中的偏移量,而function則指向一種稱爲slot function的函數,這裏新引進一個類型PyHeapTypeObject,PyHeapTypeObject但是個好東西,它在之後分析用戶自定義類型還會在用到,這裏咱們簡單看一下PyHeapTypeObject的定義,以及PyType_Type中關於PyHeapTypeObject的定義,後續還會講解PyHeapTypeObject優化
//object.h typedef struct _heaptypeobject { PyTypeObject ht_type; PyNumberMethods as_number; PyMappingMethods as_mapping; PySequenceMethods as_sequence; PyBufferProcs as_buffer; PyObject *ht_name, *ht_slots; } PyHeapTypeObject; //typeobject.c PyTypeObject PyType_Type = { …… sizeof(PyHeapTypeObject), /* tp_basicsize */ …… };
Python中提供了多個宏來定義一個slot,其中最基本的是TPSLOT和ETSLOT:spa
//typeobject.c #define TPSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ {NAME, offsetof(PyTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \ PyDoc_STR(DOC)} #define ETSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ {NAME, offsetof(PyHeapTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \ PyDoc_STR(DOC)} //structmember.h #define offsetof(type, member) ( (int) & ((type*)0) -> member )
TPSLOT和ETSLOT的區別在於TPSLOT計算的是操做對應的函數指針在PyTypeObject中的偏移量,而ETSLOT計算的是函數指針在PyHeadTypeObject中的偏移量。可是觀察下面列出的PyHeadTypeObject的代碼,能夠發現,由於PyHeadTypeObject的第一個域就是PyTypeObject,因此TPSLOT計算出的偏移量實際上也就是相對於PyHeadTypeObject的偏移量指針
對於一個PyTypeObject來講,有的操做,好比nb_add,其函數指針在PyNumberMethods中存放,而PyTypeObject中倒是經過一個tp_as_number指針指向另外一個PyNumberMethods結構,因此,實際上根本沒有辦法算出nb_add在PyTypeObject中的偏移量,只能計算其在PyHeadTypeObject這種的偏移量htm
所以,與nb_add對應的slot必須是經過ETSLOT來定義的。若是說與nb_add對應的slot中的記錄的offset是基於PyHeadTypeObject的,而PyInt_Type倒是一個PyTypeObject,那麼顯然經過這個偏移量是不可能獲得PyInt_Type中爲int準備的nb_add,那麼這個offset有什麼用呢?
其實這個offset是用來排序的,爲了理解爲何須要對操做進行排序,須要來看看Python預約義的slot集合——slotdefs
typeobject.c
…… #define ETSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ {NAME, offsetof(PyHeapTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \ PyDoc_STR(DOC)} #define SQSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ ETSLOT(NAME, as_sequence.SLOT, FUNCTION, WRAPPER, DOC) #define MPSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \ ETSLOT(NAME, as_mapping.SLOT, FUNCTION, WRAPPER, DOC) #define BINSLOT(NAME, SLOT, FUNCTION, DOC) \ ETSLOT(NAME, as_number.SLOT, FUNCTION, wrap_binaryfunc_l, \ "x." NAME "(y) <==> x" DOC "y") #define RBINSLOT(NAME, SLOT, FUNCTION, DOC) \ ETSLOT(NAME, as_number.SLOT, FUNCTION, wrap_binaryfunc_r, \ "x." NAME "(y) <==> y" DOC "x") …… static slotdef slotdefs[] = { …… //不一樣操做名對應相同操做 BINSLOT("__add__", nb_add, slot_nb_add, "+"), RBINSLOT("__radd__", nb_add, slot_nb_add, "+"), //相同操做名對應不一樣操做 SQSLOT("__getitem__", sq_item, slot_sq_item, wrap_sq_item, "x.__getitem__(y) <==> x[y]"), MPSLOT("__getitem__", mp_subscript, slot_mp_subscript, wrap_binaryfunc, "x.__getitem__(y) <==> x[y]"), …… };
其中,SQSLOT、MPSLOT、BINSLOT、RBINSLOT都是對ETSLOT的一個簡單的包裝,在slotdefs中,能夠發現,操做名(好比__add__)和操做並非一一對應的,存在着多個操做對應同一操做名的狀況,一樣也存在着同一個操做對應不一樣操做名的狀況。對於相同操做名對應不一樣操做的狀況,在填充tp_dict時,就會出現問題,好比對於__getitem__,在填充tp_dict中與其對應的是sq_item仍是mp_subscript呢?
爲了解決這個問題,就須要利用slot中的offset信息對slot(也就是對操做)進行排序。回顧一下前面的PyHeadTypeObject的代碼,它與通常的struct定義不一樣,其實定義中各個域的順序也是至關關鍵的,在順序中隱含着操做優先級的信息。好比在PyHeadTypeObject中,PyMappingMethods的位置在PySequenceMethods以前,mp_subscript是PyMappingMethods中的PyObject*,而sq_item是PySequenceMethods中的PyObject*,因此最終計算出的偏移存在以下的關係:offset(mp_subscript)<offset(sq_item)。
若是一個PyTypeObject中,既定義了mp_subscript又定義了sq_item,那麼Python虛擬機將選擇mp_subscript與__getitem__創建關係,而PyList_Type正是這樣的一個PyTypeObject,在PyList_Type中,tp_as_mapping.mp_subscript指向list_subscript,而tp_as_sequence.sq_item指向list_item
listobject.c
PyTypeObject PyList_Type = { …… &list_as_sequence, /* tp_as_sequence */ &list_as_mapping, /* tp_as_mapping */ …… }; static PySequenceMethods list_as_sequence = { …… (ssizeargfunc)list_item, /* sq_item */ …… }; static PyMappingMethods list_as_mapping = { (lenfunc)list_length, (binaryfunc)list_subscript, (objobjargproc)list_ass_subscript };
如今,讓咱們在list_item和list_subscript兩個方法中添加打印語句,看看到底是執行list_item仍是執行list_subscript
listobject.c
static PyObject * list_subscript(PyListObject* self, PyObject* item) { printf("call list_subscript\n"); if (PyIndex_Check(item)) { …… } …… } static PyObject * list_item(PyListObject *a, Py_ssize_t i) { printf("call list_item\n"); if (i < 0 || i >= a->ob_size) { …… } …… }
由於Python對list的索引元素的操做有優化,因此咱們必須用一個類繼承自list才能看到效果,A中的__getitem__對應的操做就是對PyList_Type中的mp_subscript和sq_item選擇的結果,能夠看到,list_subscript被選中了,但後面還跟着一個list_item。爲何會出現這樣的狀況?是由於在list_subscript函數中還調用了list_item
>>> a = A() >>> a.append(1) >>> print(a[0]) call list_subscript call list_item 1
slotdefs的排序在init_slotdefs中完成:
typeobject.c
static void init_slotdefs(void) { slotdef *p; static int initialized = 0; //init_slotdefs只會進行一次 if (initialized) return; for (p = slotdefs; p->name; p++) { //填充slotdef結構體中name_strobj p->name_strobj = PyString_InternFromString(p->name); if (!p->name_strobj) Py_FatalError("Out of memory interning slotdef names"); } //對slotdefs中的slotdef進行排序 qsort((void *)slotdefs, (size_t)(p-slotdefs), sizeof(slotdef), slotdef_cmp); initialized = 1; } //slot排序的比較策略 static int slotdef_cmp(const void *aa, const void *bb) { const slotdef *a = (const slotdef *)aa, *b = (const slotdef *)bb; int c = a->offset - b->offset; if (c != 0) return c; else return (a > b) ? 1 : (a < b) ? -1 : 0; }
在slot的排序策略函數slotdef_cmp中,能夠清晰地看到,slot中的offset正是操做排序的關鍵所在