Python虛擬機類機制之填充tp_dict(二)

填充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正是操做排序的關鍵所在

相關文章
相關標籤/搜索