《深度剖析CPython解釋器》20. Python類機制的深度解析(第四部分): 實例對象的創建、以及屬性訪問


楔子

介紹完類對象之后,我們來介紹實例對象。我們之前費了老鼻子勁將類對象剖析了一遍,但這僅僅是萬里長征的第一步。因為Python虛擬機執行時,在內存中興風作浪的是一個個的實例對象,而類對象只是幕后英雄。

通過class類對象創建實例對象

我們還以之前的代碼為例:

class Girl:

    name = "夏色祭"
    def __init__(self):
        print("__init__")

    def f(self):
        print("f")

    def g(self, name):
        self.name = name
        print(self.name)


girl = Girl()

看一下它的字節碼,這里我們只看創建實例對象的字節碼,也就是模塊的字節碼。

  1           0 LOAD_BUILD_CLASS
              2 LOAD_CONST               0 (<code object Girl at 0x000002B7A85FABE0, file "instance", line 1>)
              4 LOAD_CONST               1 ('Girl')
              6 MAKE_FUNCTION            0
              8 LOAD_CONST               1 ('Girl')
             10 CALL_FUNCTION            2
             12 STORE_NAME               0 (Girl)

 15          14 LOAD_NAME                0 (Girl)
             16 CALL_FUNCTION            0
             18 STORE_NAME               1 (girl)
             20 LOAD_CONST               2 (None)
             22 RETURN_VALUE

我們看到在類構建完畢之后,14 LOAD_NAME這條指令便將剛剛構建的類Girl取了出來、壓入運行時棧,然后通過CALL_FUNCTION將棧里面的類彈出、進行調用,得到實例對象,再將實例對象設置在棧頂。18 STORE_NAME將棧頂的實例對象彈出,讓符號girl與之綁定,放在local空間中。

所以我們看到調用類對象的指令居然也是CALL_FUNCTION,因為一開始我們說了,類和函數一樣,都是要先將PyCodeObject變成PyFunctionObject。

因此執行完畢之后,模塊的local空間就會變成這樣:

在CALL_FUNCTION中,Python同樣會執行對應類型的tp_call操作。所以創建實例的時候,顯然執行PyType_Type的tp_call,因此最終是在PyType_Type.tp_call中調用Girl.tp_new來創建instance對象的。

需要注意的是,在創建class Girl這個對象時,Python虛擬機調用PyType_Ready對class Girl進行了初始化,其中一項動作就是繼承基類,所以Girl.tp_new實際上就是object.tp_new,而在PyBaseObject_Type中,這個操作被定義為object_new。創建class對象和創建instance對象的不同之處正是在於tp_new不同。創建class對象,Python虛擬機使用的是tp_new,創建instance對象,Python虛擬機則使用object_new。使用類重寫__new__的話,應該很容易明白。

因此,由於我們創建的不是class對象,而是instance對象,type_call會嘗試進行初始化的動作。

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    //......
    type = Py_TYPE(obj);
    if (type->tp_init != NULL) {
        int res = type->tp_init(obj, args, kwds);
        if (res < 0) {
            assert(PyErr_Occurred());
            Py_DECREF(obj);
            obj = NULL;
        }
        else {
            assert(!PyErr_Occurred());
        }
    }
    return obj;
}

那么這個tp_init是哪里來的的,是在使用tp_new創建類對象的時候來的,tp_init在PyType_Ready時會繼承PyBaseObject_Type的object_init操作。但正如我們之前說的那樣,因為A中的定義重寫了__init__,所以在 fixup_slot_dispatchers 中,tp_init會指向slotdef中指定的與__init__對應的slot_tp_init。並且還會設置tp_alloc,這與內存分配有關,源碼中會有所體現。

static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
    //........
	
    //tp_alloc被設置為PyType_GenericAlloc, 表示為實例對象分配內存, 因為內存大小的元信息存在對應的類對象中
    //並且在分配內存的同時會將實例對象的ob_type設置為對應的類對象
    type->tp_alloc = PyType_GenericAlloc;
    if (type->tp_flags & Py_TPFLAGS_HAVE_GC) {
        type->tp_free = PyObject_GC_Del;
        type->tp_traverse = subtype_traverse;
        type->tp_clear = subtype_clear;
    }
    else
        type->tp_free = PyObject_Del;
	
    //設置tp_init
    fixup_slot_dispatchers(type);
    //......
}


PyObject *
PyType_GenericAlloc(PyTypeObject *type, Py_ssize_t nitems)
{
    PyObject *obj;
    const size_t size = _PyObject_VAR_SIZE(type, nitems+1);
    
    //分配內存
    if (PyType_IS_GC(type))
        obj = _PyObject_GC_Malloc(size);
    else
        obj = (PyObject *)PyObject_MALLOC(size);

    if (obj == NULL)
        return PyErr_NoMemory();

    memset(obj, '\0', size);
	
    //設置實例對象的ob_type
    if (type->tp_itemsize == 0)
        (void)PyObject_INIT(obj, type);
    else
        (void) PyObject_INIT_VAR((PyVarObject *)obj, type, nitems);

    if (PyType_IS_GC(type))
        _PyObject_GC_TRACK(obj);
    return obj;
}

而在 slot_tp_init 中又做了哪些事情呢?

static int
slot_tp_init(PyObject *self, PyObject *args, PyObject *kwds)
{
    _Py_IDENTIFIER(__init__);
    int unbound;
    //虛擬機會通過lookup_method從class對象及其mro列表中搜索屬性__init__對應的操作
    PyObject *meth = lookup_method(self, &PyId___init__, &unbound);
    //返回結果
    PyObject *res;

    if (meth == NULL)
        return -1;
    //執行
    if (unbound) {
        res = _PyObject_Call_Prepend(meth, self, args, kwds);
    }
    else {
        res = PyObject_Call(meth, args, kwds);
    }
    Py_DECREF(meth);
    if (res == NULL)
        return -1;
    //如果返回的不是None,那么報錯,這個信息熟悉不
    if (res != Py_None) {
        PyErr_Format(PyExc_TypeError,
                     "__init__() should return None, not '%.200s'",
                     Py_TYPE(res)->tp_name);
        Py_DECREF(res);
        return -1;
    }
    Py_DECREF(res);
    return 0;
}

所以如果你在定義class時,重寫了__init__函數,那么創建實例對象時搜索的結果就是你寫的函數,如果沒有重寫那么執行object的__init__操作,而在object的__init__中,Python虛擬機則什么也不做,而是直接返回。

到了這里可以小結一下,從class對象創建instance對象的兩個步驟:

  • instance = class.__new__(class, *args, **kwargs)
  • class.__init__(instance, *args, **kwargs)

需要注意的是,這兩個步驟同樣也適用於從metaclass對象創建class對象,因為從metaclass對象創建class對象的過程其實和class對象創建instance對象是一樣的,我們說class具有二象性。

訪問instance對象中的屬性

在前面的章節中我們討論名字空間時就提到,在Python中,形如x.y形式的表達式稱之為"屬性引用",其中x為對象,y為對象的某個屬性,這個屬性可以是很多種,比如:整數、字符串、函數、類、甚至是模塊等等。

class Girl:

    name = "夏色祭"
    def __init__(self):
        print("__init__")

    def f(self):
        print("f")

    def g(self, name):
        self.name = name
        print(self.name)


girl = Girl()
girl.f()
girl.g("神樂mea")

我們加上屬性查找邏輯,看看它的字節碼如何。

  1           0 LOAD_BUILD_CLASS
              2 LOAD_CONST               0 (<code object Girl at 0x0000019158F5ABE0, file "instance", line 1>)
              4 LOAD_CONST               1 ('Girl')
              6 MAKE_FUNCTION            0
              8 LOAD_CONST               1 ('Girl')
             10 CALL_FUNCTION            2
             12 STORE_NAME               0 (Girl)

 15          14 LOAD_NAME                0 (Girl)
             16 CALL_FUNCTION            0
             18 STORE_NAME               1 (girl)

 16          20 LOAD_NAME                1 (girl)
             22 LOAD_METHOD              2 (f)
             24 CALL_METHOD              0
             26 POP_TOP

 17          28 LOAD_NAME                1 (girl)
             30 LOAD_METHOD              3 (g)
             32 LOAD_CONST               2 ('神樂mea')
             34 CALL_METHOD              1
             36 POP_TOP
             38 LOAD_CONST               3 (None)
             40 RETURN_VALUE

  • 20 LOAD_NAME: 加載變量girl, 因為是girl.f, 所以首先要把girl加載進來, 也就是壓入運行時棧;
  • 22 LOAD_METHOD: 我們看到了一個新的指令, LOAD_METHOD, 顯然這是加載一個方法, 關於函數和方法的區別我們后面會詳細說;
  • 24 CALL_METHOD: 調用方法;
  • 26 POP_TOP: 從棧頂將元素彈出;
  • 32 LOAD_CONST: 除了加載girl和g之外, 還要加載一個常量字符串;
  • 34 CALL_METHOD: 調用方法, 這里參數是1個;

所以關鍵指令就在於LOAD_METHOD和CALL_METHOD,我們先來看看LOAD_METHOD都做了什么吧。

        case TARGET(LOAD_METHOD): {
            //從符號表中獲取符號, 如果是girl.f的話, 那么這個name就是一個PyUnicodeObject對象"f"
            PyObject *name = GETITEM(names, oparg);
            //從棧頂獲取(不是彈出, 彈出是POP)obj, 顯然這個obj就是實例對象girl
            PyObject *obj = TOP();
            //meth是一個PyObject *指針,顯然它要指向一個方法
            PyObject *meth = NULL;
			
            //這里是獲取obj中和符號name綁定的方法,然后讓meth指向它
            //傳入二級指針&meth,然后讓meth存儲的地址變成指向具體方法的地址
            int meth_found = _PyObject_GetMethod(obj, name, &meth);
			
            //如果meth == NULL,raise AttributeError
            if (meth == NULL) {
                /* Most likely attribute wasn't found. */
                goto error;
            }
			
            //另外還返回了一個meth_found, 要么為1、要么為0
            if (meth_found) {
                //如果meth_found為1,說明meth是一個未綁定的方法,obj就是self
                //關於綁定和未綁定我們后面會詳細介紹
                SET_TOP(meth);
                PUSH(obj);  // self
            }
            else {
                //否則meth不是一個未綁定的方法,而是一個描述符協議返回的一個普通屬性、亦或是其他的什么東西
                //那么棧的第二個元素就會設置為NULL
                SET_TOP(NULL);
                Py_DECREF(obj);
                PUSH(meth);
            }
            DISPATCH();
        }

獲取方法是LOAD_METHOD,那么獲取屬性呢?對,其實肯定有人想到了,獲取屬性是LOAD_ATTR。

        case TARGET(LOAD_ATTR): {
            //可以看到這個和LOAD_METHOD本質上是類似的,並且還要更簡單一些
            //name依舊是符號
            PyObject *name = GETITEM(names, oparg);
            //owner是所有者,為什么不叫obj,因為方法都是給實例用的,盡管類也能調用,但是方法畢竟是給實例用的
            //但是屬性的話,類和實例都可以訪問,各自互不干擾,所以是owner
            PyObject *owner = TOP();
            //res顯然就是獲取屬性返回的結果了, 通過PyObject_GetAttr進行獲取
            PyObject *res = PyObject_GetAttr(owner, name);
            Py_DECREF(owner);
            //設置到棧頂
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

LOAD_ATTR和LOAD_METHOD這兩個指令集我們都看到了,但是里面具體實現的方法還沒有看,LOAD_ATTR調用了 PyObject_GetAttr 函數,LOAD_METHOD調用了 _PyObject_GetMethod ,我們來看看這兩個方法都長什么樣子。首先就從 PyObject_GetAttr 開始。

//Objects/object.c
PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{	
    //v: 對象
    //name: 屬性名
    
    //獲取類型對象
    PyTypeObject *tp = Py_TYPE(v);
	
    //name必須是一個字符串
    if (!PyUnicode_Check(name)) {
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return NULL;
    }
    //通過類型對象的tp_getattro獲取對應的屬性, 實例獲取屬性(包括方法)的時候都是通過類來獲取的
    //girl.f()本質上就是Girl.f(girl), 但是后者是不是長得有點丑啊, 所以Python提供了girl.f()
    //並且我們也看到了, 實例調用方法的時候會自動將自身作為參數傳進去, 而類默認則不會
    //也正因為如此類獲取的話(Girl.f)叫函數, 實例獲取(girl.f)的話叫方法, 后面會介紹
    if (tp->tp_getattro != NULL)
        return (*tp->tp_getattro)(v, name);
    
    //通過tp_getattr獲取屬性對應的對象, 這里的name是一個char *, 而tp_getattro是一個PyObject *
    //顯然tp_getattro還可以處理中文的情況, 只不過我們不會使用中文來命名就是了
    if (tp->tp_getattr != NULL) {
        const char *name_str = PyUnicode_AsUTF8(name);
        if (name_str == NULL)
            return NULL;
        return (*tp->tp_getattr)(v, (char *)name_str);
    }
    
    //屬性不存在,拋出異常
    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%U'",
                 tp->tp_name, name);
    return NULL;
}

在Python的class對象中,定義了兩個與屬性訪問相關的操作:tp_getattro和tp_getattr。其中tp_getattro是優先選擇的屬性訪問動作,而tp_getattr在Python中已不推薦使用。而這兩者的區別在 PyObject_GetAttr 中已經顯示的很清楚了,主要是在屬性名的使用上,tp_getattro所使用的屬性名必須是一個PyUnicodeObject對象,而tp_getattr所使用的屬性名必須是一個char *。因此如果某個類型定義了tp_getattro和tp_getattr,那么 PyObject_GetAttr 優先使用tp_getattro,因為這位老鐵寫在上面。

在Python虛擬機創建class Girl時,會從PyBaseObject_Type中繼承其tp_getattro->PyObject_GenericGetAttr,所以Python虛擬機又會在這里進入 PyObject_GenericGetAttr ,並且 PyObject_GenericGetAttr 正好涉及到了Python中的描述符,因此也為我們我們后面介紹描述符埋下了一個伏筆。

//Objects/object.c
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}


PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name,
                                 PyObject *dict, int suppress)
{
    //拿到obj的類型,對於我們的例子來說, 顯然是class Girl
    PyTypeObject *tp = Py_TYPE(obj);
    //一個描述符對象
    PyObject *descr = NULL;
    PyObject *res = NULL;
    descrgetfunc f;
    Py_ssize_t dictoffset;
    PyObject **dictptr;
	
    //name必須是str
    if (!PyUnicode_Check(name)){
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return NULL;
    }
    Py_INCREF(name);
	
    //字典為空則進行初始化
    if (tp->tp_dict == NULL) {
        if (PyType_Ready(tp) < 0)
            goto done;
    }
	
    //嘗試從mro列表中拿到符號對應的值,等價於descr = Girl.符號 if hasattr(Girl, '符號') else NULL
    descr = _PyType_Lookup(tp, name);

    f = NULL;
    if (descr != NULL) {
        Py_INCREF(descr);
        //f = descr.__class__.__get__ 
        f = descr->ob_type->tp_descr_get;
        if (f != NULL && PyDescr_IsData(descr)) {
            //f不為NULL,並且descr是數據描述符,那么直接將描述符中__get__方法的結果返回
            //這個f就是描述符里面的__get__方法,而這個descr就是描述符的一個實例對象
            res = f(descr, obj, (PyObject *)obj->ob_type);
            if (res == NULL && suppress &&
                    PyErr_ExceptionMatches(PyExc_AttributeError)) {
                PyErr_Clear();
            }
            goto done;
        }
    }
	
    //那么顯然要從instance對象自身的__dict__中尋找屬性
    if (dict == NULL) {
        /* Inline _PyObject_GetDictPtr */
        dictoffset = tp->tp_dictoffset;
        //但如果dict為NULL,並且dictoffset不為0, 說明繼承自變長對象,那么要調整tp_dictoffset
        if (dictoffset != 0) {
            if (dictoffset < 0) {
                Py_ssize_t tsize;
                size_t size;

                tsize = ((PyVarObject *)obj)->ob_size;
                if (tsize < 0)
                    tsize = -tsize;
                size = _PyObject_VAR_SIZE(tp, tsize);
                _PyObject_ASSERT(obj, size <= PY_SSIZE_T_MAX);

                dictoffset += (Py_ssize_t)size;
                _PyObject_ASSERT(obj, dictoffset > 0);
                _PyObject_ASSERT(obj, dictoffset % SIZEOF_VOID_P == 0);
            }
            dictptr = (PyObject **) ((char *)obj + dictoffset);
            dict = *dictptr;
        }
    }
    //dict不為NULL,從字典中獲取
    if (dict != NULL) {
        Py_INCREF(dict);
        res = PyDict_GetItemWithError(dict, name);
        if (res != NULL) {
            Py_INCREF(res);
            Py_DECREF(dict);
            goto done;
        }
        else {
            Py_DECREF(dict);
            if (PyErr_Occurred()) {
                if (suppress && PyErr_ExceptionMatches(PyExc_AttributeError)) {
                    PyErr_Clear();
                }
                else {
                    goto done;
                }
            }
        }
    }
	
    //我們看到這里又判斷了一次,但是這次少了個條件
    //沒錯熟悉Python描述符的應該知道,上面的需要滿足是數據描述符
    //這個是非數據描述符
    if (f != NULL) {
        res = f(descr, obj, (PyObject *)Py_TYPE(obj));
        if (res == NULL && suppress &&
                PyErr_ExceptionMatches(PyExc_AttributeError)) {
            PyErr_Clear();
        }
        goto done;
    }
	
    //返回
    if (descr != NULL) {
        res = descr;
        descr = NULL;
        goto done;
    }
	
    //找不到,就報錯
    if (!suppress) {
        PyErr_Format(PyExc_AttributeError,
                     "'%.50s' object has no attribute '%U'",
                     tp->tp_name, name);
    }
  done:
    Py_XDECREF(descr);
    Py_DECREF(name);
    return res;
}

屬性訪問是從 PyObject_GetAttr 開始,那么下面我們來看看 _PyObject_GetMethod 生的什么模樣,其實不用想也知道,它和 PyObject_GetAttr 高度相似。

//Objects/object.c
int
_PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method)
{
    PyTypeObject *tp = Py_TYPE(obj);
    PyObject *descr;
    descrgetfunc f = NULL;
    PyObject **dictptr, *dict;
    PyObject *attr;
    int meth_found = 0;

    assert(*method == NULL);

    if (Py_TYPE(obj)->tp_getattro != PyObject_GenericGetAttr
            || !PyUnicode_Check(name)) {
        *method = PyObject_GetAttr(obj, name);
        return 0;
    }

    if (tp->tp_dict == NULL && PyType_Ready(tp) < 0)
        return 0;

    descr = _PyType_Lookup(tp, name);
    if (descr != NULL) {
        Py_INCREF(descr);
        if (PyType_HasFeature(Py_TYPE(descr), Py_TPFLAGS_METHOD_DESCRIPTOR)) {
            meth_found = 1;
        } else {
            f = descr->ob_type->tp_descr_get;
            if (f != NULL && PyDescr_IsData(descr)) {
                *method = f(descr, obj, (PyObject *)obj->ob_type);
                Py_DECREF(descr);
                return 0;
            }
        }
    }

    dictptr = _PyObject_GetDictPtr(obj);
    if (dictptr != NULL && (dict = *dictptr) != NULL) {
        Py_INCREF(dict);
        attr = PyDict_GetItemWithError(dict, name);
        if (attr != NULL) {
            Py_INCREF(attr);
            *method = attr;
            Py_DECREF(dict);
            Py_XDECREF(descr);
            return 0;
        }
        else {
            Py_DECREF(dict);
            if (PyErr_Occurred()) {
                Py_XDECREF(descr);
                return 0;
            }
        }
    }

    if (meth_found) {
        *method = descr;
        return 1;
    }

    if (f != NULL) {
        *method = f(descr, obj, (PyObject *)Py_TYPE(obj));
        Py_DECREF(descr);
        return 0;
    }

    if (descr != NULL) {
        *method = descr;
        return 0;
    }

    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%U'",
                 tp->tp_name, name);
    return 0;
}

非常類似,這里就不介紹了。

實例對象的屬性字典

在屬性訪問的時候,我們可以通過girl.__dict__這種形式訪問。但是這就奇怪了,在之前的描述中,我們看到從class Girl創建instance girl的時候,Python並沒有為instance創建PyDictObject對象啊。

但是在上一篇介紹metaclass的時候,我們說過這樣一句話,對於任意繼承object的class對象來說,這個大小為PyBaseObject_Type->tp_basicsize + 16,其中的16是2 * sizeof(PyObject *)。后面跟着的兩個PyObject *的空間被設置給了tp_dictoffset和tp_weaklistoffset,那么現在是時候揭開謎底了。

在創建class類對象時我們曾說,Python虛擬機設置了一個名為tp_dictoffset的域,從名字推斷,這個可能就是instance對象中__dict__的偏移位置。

虛線中畫出的dict對象就是我們期望中的實例對象的屬性字典,這個猜想可以在PyObject_GenericGetAttr中得到證實。

//object.c
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
    return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}

PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name,
                                 PyObject *dict, int suppress)
{
    //那么顯然要從instance對象自身的__dict__中尋找屬性
    if (dict == NULL) {
        /* Inline _PyObject_GetDictPtr */
        dictoffset = tp->tp_dictoffset;
        if (dictoffset != 0) {
            //但如果dict為NULL,並且dictoffset說明繼承自變長對象,那么要調整tp_dictoffset
            if (dictoffset < 0) {
                Py_ssize_t tsize;
                size_t size;

                tsize = ((PyVarObject *)obj)->ob_size;
                if (tsize < 0)
                    tsize = -tsize;
                size = _PyObject_VAR_SIZE(tp, tsize);
                assert(size <= PY_SSIZE_T_MAX);

                dictoffset += (Py_ssize_t)size;
                assert(dictoffset > 0);
                assert(dictoffset % SIZEOF_VOID_P == 0);
            }
            dictptr = (PyObject **) ((char *)obj + dictoffset);
            dict = *dictptr;
        }
    }

如果dictoffset小於0,意味着Girl是繼承自類似str這樣的變長對象,Python虛擬機會對dictoffset做一些處理,最終仍然會使dictoffset指向a的內存中額外申請的位置。而PyObject_GenericGetAttr正是根據這個dictoffset獲得了一個dict對象。更近一步,我們發現函數g中有設置self.name的代碼,這個instance對象的屬性設置也會訪問屬性字典,而這個設置的動作最終會調用 PyObject_GenericSetAttr ,也就是girl.__dict__最初被創建的地方。

//object.c
int
PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value)
{
    return _PyObject_GenericSetAttrWithDict(obj, name, value, NULL);
}


int
_PyObject_GenericSetAttrWithDict(PyObject *obj, PyObject *name,
                                 PyObject *value, PyObject *dict)
{
    PyTypeObject *tp = Py_TYPE(obj);
    PyObject *descr;
    descrsetfunc f;
    PyObject **dictptr;
    int res = -1;
	
    //老規矩,name必須是PyUnicodeObject對象
    if (!PyUnicode_Check(name)){
        PyErr_Format(PyExc_TypeError,
                     "attribute name must be string, not '%.200s'",
                     name->ob_type->tp_name);
        return -1;
    }
	
    //字典為空、則進行初始化
    if (tp->tp_dict == NULL && PyType_Ready(tp) < 0)
        return -1;

    Py_INCREF(name);
	
    //老規矩,獲取屬性
    descr = _PyType_Lookup(tp, name);

    if (descr != NULL) {
        Py_INCREF(descr);
        f = descr->ob_type->tp_descr_set;
        if (f != NULL) {
            res = f(descr, obj, value);
            goto done;
        }
    }

    if (dict == NULL) {
        //這行代碼就是PyObject_GenericGetAttr中根據dictoffset獲取dict對象的那段代碼
        dictptr = _PyObject_GetDictPtr(obj);
        if (dictptr == NULL) {
            if (descr == NULL) {
                PyErr_Format(PyExc_AttributeError,
                             "'%.100s' object has no attribute '%U'",
                             tp->tp_name, name);
            }
            else {
                PyErr_Format(PyExc_AttributeError,
                             "'%.50s' object attribute '%U' is read-only",
                             tp->tp_name, name);
            }
            goto done;
        }
        res = _PyObjectDict_SetItem(tp, dictptr, name, value);
    }
    else {
        Py_INCREF(dict);
        if (value == NULL)
            res = PyDict_DelItem(dict, name);
        else
            res = PyDict_SetItem(dict, name, value);
        Py_DECREF(dict);
    }
    if (res < 0 && PyErr_ExceptionMatches(PyExc_KeyError))
        PyErr_SetObject(PyExc_AttributeError, name);

  done:
    Py_XDECREF(descr);
    Py_DECREF(name);
    return res;
}

再論descriptor

前面我們看到,在 PyType_Ready 中,Python虛擬機會填充tp_dict,其中與操作名對應的是一個個descriptor(描述符),那時我們看到的是descriptor這個概念在Python內部是如何實現的。現在我們將要剖析的是descriptor在Python的類機制中究竟會起到怎樣的作用。

在Python虛擬機對class對象或instance對象進行屬性訪問時,descriptor將對屬性訪問的行為產生重大的影響。一般而言,對於一個對象obj,如果obj.__class__對應的class對象中存在__get__、__set__、__delete__操作(不要求三者同時存在),那么obj便可以稱之為描述符。在slotdefs中,我們會看到這三種魔法方法對應的操作。

 
//typeobject,c

    TPSLOT("__get__", tp_descr_get, slot_tp_descr_get, wrap_descr_get,
           "__get__($self, instance, owner, /)\n--\n\nReturn an attribute of instance, which is of type owner."),
    TPSLOT("__set__", tp_descr_set, slot_tp_descr_set, wrap_descr_set,
           "__set__($self, instance, value, /)\n--\n\nSet an attribute of instance to value."),
    TPSLOT("__delete__", tp_descr_set, slot_tp_descr_set,
           wrap_descr_delete,
           "__delete__($self, instance, /)\n--\n\nDelete an attribute of instance."),

前面我看到了 PyWrapperDescrObjectPyMethodDescrObject 等對象,它們對應的類對象中分別為tp_descr_get設置了wrapperdescr_get,method_get等函數,所以它們是當之無愧的descriptor。

另外如果細分,descriptor還可以分為兩種。

關於python中的描述符,我這里有一篇博客寫的很詳細,對描述符機制不太懂的話可以先去看看,https://www.cnblogs.com/traditional/p/11714356.html。

  • data descriptor:數據描述符,對應的__class__中定義了__get__和__set__的descriptor

  • no data descriptor:非數據描述符,對應的__class__中只定義了__get__方法。

在Python虛擬機訪問instance對象的屬性時,descriptor的一個作用就是影響Python虛擬機對屬性的選擇。從 PyObject_GenericGetAttr 源碼中可以看到,Python虛擬機會在instance對象自身的__dict__中尋找屬性,也會在instance對象對應的class對象的mro列表中尋找屬性,我們將前一種屬性稱之為instance屬性,后一種屬性稱之為class屬性。在屬性的選擇上,有如下規律:

  • Python虛擬機優先按照instance屬性、class屬性的順序選擇屬性,即instance屬性優先於class屬性
  • 如果在class屬性中發現同名的data descriptor,那么該descriptor會優先於instance屬性被Python虛擬機選擇

這兩條規則在對屬性進行設置時仍然會被嚴格遵守,換句話說,如果執行girl.value = 1,而在Girl中出現了名為value的數據描述符,那么不好意思,會執行__set__方法,如果是非數據描述符,那么就不再走__set__了,而是設置屬性,相當於a.__dict__['value'] = 1

所以,獲取被描述符代理的屬性時,會直接調用__get__方法。設置的話,會調用__set__。當然要考慮優先級的問題,至於優先級的問題是什么,這里就不再解釋,強烈建立看我上面發的博客鏈接,對描述符的解析很詳細。

函數變身

在Girl的成員f對應的def語句中,我們分明一個self參數,那么self在Python中是不是一個真正有效的參數呢?還是它僅僅只是一個語法意義是占位符而已?這一點可以從函數g中看到答案,在g中有這樣的語句:self.name = name,這條語句毫無疑問地揭示了self確實是一個實實在在的對象,所以表面上看起來f是一個不需要參數的函數,但實際上是一個貨真價值的帶參函數,只不過第一個參數自動幫你傳遞了。根據使用Python的經驗,我們都知道,傳遞給self的就是實例本身。但是現在問題來了,這是怎么實現的呢?我們先再看一遍字節碼:

  1           0 LOAD_BUILD_CLASS
              2 LOAD_CONST               0 (<code object Girl at 0x0000019D7B4EABE0, file "instance", line 1>)
              4 LOAD_CONST               1 ('Girl')
              6 MAKE_FUNCTION            0
              8 LOAD_CONST               1 ('Girl')
             10 CALL_FUNCTION            2
             12 STORE_NAME               0 (Girl)

 15          14 LOAD_NAME                0 (Girl)
             16 CALL_FUNCTION            0
             18 STORE_NAME               1 (girl)

 16          20 LOAD_NAME                1 (girl)
             22 LOAD_METHOD              2 (f)
             24 CALL_METHOD              0
             26 POP_TOP

 17          28 LOAD_NAME                1 (girl)
             30 LOAD_METHOD              3 (g)
             32 LOAD_CONST               2 ('神樂mea')
             34 CALL_METHOD              1
             36 POP_TOP
             38 LOAD_CONST               3 (None)
             40 RETURN_VALUE

我們看一下:24 CALL_METHOD,我們說會將girl.f壓入運行時棧,然后就執行CALL_METHOD指令了,注意這里的oparg是0,表示不需要參數(不需要我們傳遞參數)。注意:這里是CALL_METHOD,不是CALL_FUNCTION。因此我們可以有兩條路可走,一條是看看CALL_METHOD是什么,另一條是再研究一下PyFunctionObject。我們先來看看CALL_METHOD這個指令長什么樣子吧。

        case TARGET(CALL_METHOD): {
            /* Designed to work in tamdem with LOAD_METHOD. */
            PyObject **sp, *res, *meth;

            sp = stack_pointer;

            meth = PEEK(oparg + 2);
            if (meth == NULL) {
                res = call_function(tstate, &sp, oparg, NULL);
                stack_pointer = sp;
                (void)POP(); /* POP the NULL. */
            }
            else {
                res = call_function(tstate, &sp, oparg + 1, NULL);
                stack_pointer = sp;
            }

            PUSH(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }


//為了對比,我們再把CALL_FUNCTION的源碼貼出來
        case TARGET(CALL_FUNCTION): {
            PREDICTED(CALL_FUNCTION);
            PyObject **sp, *res;
            sp = stack_pointer;
            res = call_function(tstate, &sp, oparg, NULL);
            stack_pointer = sp;
            PUSH(res);
            if (res == NULL) {
                goto error;
            }
            DISPATCH();
        }

通過對比,發現端倪,這兩個都調用了call_function,但是傳遞的參數不一樣,call_function的第二個參數一個oparg+1(猜測第一個給了self),一個是oparg,但是這還不足以支持我們找出問題所在。其實在剖析函數的時候,我們放過了PyFunctionObject的ob_type ->PyFunction_Type。在這個PyFunction_Type中,隱藏着一個驚天大秘密。

PyTypeObject PyFunction_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "function",
    sizeof(PyFunctionObject),
    //...
    //...
    
    //注意注意注意注意注意注意注意,看下面這行
    func_descr_get,                             /* tp_descr_get */
    0,                                          /* tp_descr_set */
    offsetof(PyFunctionObject, func_dict),      /* tp_dictoffset */
    0,                                          /* tp_init */
    0,                                          /* tp_alloc */
    func_new,                                   /* tp_new */
};


我們發現 tp_descr_get 被設置成了func_descr_get,這意味着我們得到的是一個描述符。另外由於 girl.__dict__ 中沒有f,那么 girl.f 的返回值將會被 descriptor 改變,也就是 func_descr_get(Girl.f, girl, Girl)

//funcobject.c
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{	
    //如果是類獲取函數, 那么這里的obj就是NULL, type就是類對象本身
    //如果是實例獲取函數, 那么這里的obj就是實例對象, type仍是類對象本身
    
    //如果obj為空, 說明是類獲取, 那么直接返回func本身, 也就是原來的函數
    if (obj == Py_None || obj == NULL) {
        Py_INCREF(func);
        return func;
    }
    //如果是實例對象, 那么通過PyMethod_New將函數和實例綁定在一起, 得到一個PyMethodObject對象
    return PyMethod_New(func, obj);
}

func_descr_get將Girl.f對應的PyFunctionObject進行了一番包裝,所以通過PyMethod_New,Python虛擬機在PyFunctionObject的基礎上創建一個新的對象PyMethodObject,那么這個PyMethodObject是什么呢?到PyMethod_New中一看,這個神秘的對象就現身了:

//classobjet.c
PyObject *
PyMethod_New(PyObject *func, PyObject *self)
{
    PyMethodObject *im; //PyMethodObject對象的指針
    if (self == NULL) {
        PyErr_BadInternalCall();
        return NULL;
    }
    im = free_list;
    //使用緩沖池
    if (im != NULL) {
        free_list = (PyMethodObject *)(im->im_self);
        (void)PyObject_INIT(im, &PyMethod_Type);
        numfree--;
    }
    //不使用緩沖池,直接創建PyMethodObject對象
    else {
        im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
        if (im == NULL)
            return NULL;
    }
    im->im_weakreflist = NULL;
    Py_INCREF(func);
    //im_func指向PyFunctionObject對象
    im->im_func = func; 
    Py_XINCREF(self);
    //im_self指向實例對象
    im->im_self = self;
    im->vectorcall = method_vectorcall;
    _PyObject_GC_TRACK(im);
    return (PyObject *)im;
}

一切真相大白,原來那個神秘的對象就是PyMethodObject對象,看到free_list這樣熟悉的字眼,我們就知道Python內部對PyMethodObject的實現和管理中使用緩沖池的技術。現在再來看看這個PyMethodObject:

//classobject.h
typedef struct {
    PyObject_HEAD
    //可調用的PyFunctionObject對象
    PyObject *im_func;  
    //self參數,instance對象
    PyObject *im_self;   
    //弱引用列表
    PyObject *im_weakreflist; /* List of weak references */
    vectorcallfunc vectorcall;
} PyMethodObject;

在PyMethod_New中,分別將im_func,im_self設置了不同的值,分別是:f對應PyFunctionObject對象、實例girl對應的instance對象。因此通過PyMethodObject對象將PyFunctionObject對象和instance對象結合在一起,而這個PyMethodObject對象就是我們說的方法。

不管是類還是實例,獲取成員函數都會走描述符的 func_descr_get,在里面會判斷是類獲取還是實例獲取。如果是類獲取,那么直接返回函數本身,如果實例獲取則會通過PyMethod_New將func和instance綁定起來得到PyMethodObject對象,再調用函數的時候其實調用的是PyMethodObject。當調用PyMethodObject中會處理自動傳參的邏輯,將instance和我們傳遞的參數組合起來(如果我們沒有傳參, 那么只有一個self),然后整體傳遞給PyFunctionObject,所以為什么實例調用方法的時候會自動傳遞第一個參數現在是真相大白了。

這個過程稱之為成員函數的綁定,就是將實例和函數綁定起來,使之成為一個整體(方法)

class Girl:
    name = "夏色祭"

    def __init__(self):
        print("__init__")

    def f(self):
        print("f")

    def g(self, name):
        self.name = name
        print(self.name)


girl = Girl()
print(Girl.f)  # <function Girl.f at 0x000001B7805A2820>
print(girl.f)  # <bound method Girl.f of <__main__.Girl object at 0x000001B7E92282B0>>

print(type(Girl.f))  # <class 'function'>
print(type(girl.f))  # <class 'method'>

我們看到通過類來調用成員的函數得到的就是一個普通的函數,如果是實例調用成員函數,那么會將成員函數包裝成一個方法,也就是將成員函數和實例綁定在一起,得到結果就是方法,實現方式是通過描述符。

方法調用

在LOAD_METHOD指令結束之后,那么便開始了CALL_METHOD,我們知道這個和CALL_FUNCTION之間最大的區別就是,CALL_METHOD調用的是一個PyMethodObject對象,而CALL_FUNCTION調用的一個PyFunctionObject對象。

CALL_METHOD底層也調用了CALL_FUNCTION,因為方法是將函數和實例綁定在了一起,但最終執行的還是函數。

//ceval.c
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION
call_function(PyThreadState *tstate, PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{
    //......
    if (tstate->use_tracing) {
        x = trace_call_function(tstate, func, stack, nargs, kwnames);
    }
    //......
    return x;
}


static PyObject *
trace_call_function(PyThreadState *tstate,
                    PyObject *func,
                    PyObject **args, Py_ssize_t nargs,
                    PyObject *kwnames)
{
    PyObject *x; //返回值
    
    //如果func是一個函數, 那么直接通過_PyObject_Vectorcall進行調用, 然后將返回值設置給x
    if (PyCFunction_Check(func)) {
        C_TRACE(x, _PyObject_Vectorcall(func, args, nargs, kwnames));
        return x;
    }
    
    //如果func是一個描述符, 注意:此時的 func 不是方法,方法的類型是 PyMethod_Type,這里是描述符
    //那么nargs(func的參數個數)必須大於0, 因為默認會傳遞一個self
    else if (Py_TYPE(func) == &PyMethodDescr_Type && nargs > 0) {
        /* We need to create a temporary bound method as argument
           for profiling.

           If nargs == 0, then this cannot work because we have no
           "self". In any case, the call itself would raise
           TypeError (foo needs an argument), so we just skip
           profiling. */
        PyObject *self = args[0]; //self就是args的第一個參數
        //通過調用 PyMethodDescr_Type 的tp_descr_get, 接收三個參數: 函數(顯然是通過類獲取的)、實例對象、類對象
        //然后調用該描述符的 __get__ 方法,獲取返回值
        func = Py_TYPE(func)->tp_descr_get(func, self, (PyObject*)Py_TYPE(self));
        if (func == NULL) {
            return NULL;
        }
        C_TRACE(x, _PyObject_Vectorcall(func, //調整參數信息變量
                                        args+1, nargs-1,
                                        kwnames));
        Py_DECREF(func);
        return x;
    }
    // 說明是一個方法,還是走同樣的邏輯,在里面會自動處理參數邏輯
    return _PyObject_Vectorcall(func, args, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, kwnames);
}

所以函數調用和方法調用本質上都是類似的,方法里面的成員im_func指向一個函數。調用方法的時候底層還是會調用函數,只不過在調用的時候會自動把方法里面的im_self作為第一個參數傳到函數里面去。而我們通過類調用的時候,調用的就是一個普通的函數,所以第一個參數需要我們手動傳遞。

因此到了這里,我們可以在更高層次俯視一下Python的運行模型了,最核心的模型非常簡單,可以簡化為兩條規則:

  • 1. 在某個名字空間中尋找符號對應的對象
  • 2. 對得到的對象進行某些操作

拋開面向對象這些花里胡哨的外表,其實我們發現class類對象其實就是一個名字空間,實例對象也是一個名字空間,不過這些名字空間通過一些特殊的規則連接在一起,使得符號的搜索過程變得復雜,從而實現了面向對象這種編程模式,それだけ。

bound method和unbound method

在Python中,當對作為方法(或者說作為屬性的函數)進行引用時,會有兩種形式,bound method和unbound method。

  • bound method:這種形式是通過實例對象進行屬性引用,就像我們之前說的a.f這樣
  • unbound method:這種形式是通過類對象進行屬性引用,比如A.f

在Python中,bound method和unbound method的本質區別就在於PyFunctionObject有沒有和對象綁定在一起,成為PyMethodObject對象。bound method完成了綁定動作,而unbound method沒有完成綁定動作。

所以無論是類還是實例,在調用成員函數的時候都會經過func_descr_get,但如果是類調用obj為NULL,實例對象調用obj就是實例。而obj如果為NULL,那么就直接返回了,否則通過PyMethod_New變成一個方法。

//funcobject.c
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{	
    if (obj == Py_None || obj == NULL) {
        Py_INCREF(func);
        return func;
    }
    return PyMethod_New(func, obj);
}

我們通過Python演示一下:

class Descr:

    def __init__(self, *args):
        pass

    def __get__(self, instance, owner):
        print(instance)
        print(owner)



class Girl:
    
    @Descr
    def f(self):
        pass


Girl.f
"""
None
<class '__main__.Girl'>
"""
Girl().f
"""
<__main__.Girl object at 0x000001BDEE7A85E0>
<class '__main__.Girl'>
"""

從Python的層面上我們也可以看到區別。

所以在對unbound method進行調用時,我們必須要顯示的傳遞一個對象(這個對象可以任意,具體什么意思后面會演示)作為成員函數的第一個參數,因為f無論如何都需要一個self參數,所以本質上就是Girl.f(girl)這種形式。而無論是對unbound method進行調用,還是對bound method進行調用,Python虛擬機的動作本質都是一樣的,都是調用帶位置參數的一般函數。區別只在於:當調用bound method時,由於Python虛擬機幫我們完成了PyFunctionObject對象和調用者的綁定,調用者將自動成為self參數;而調用unbound method時,沒有這個綁定,我們需要自己傳入self參數。

class Girl(object):

    def f(self):
        print(self)


girl = Girl()
Girl.f(123)  # 123
# 我們看到即便傳入一個123也是可以的
# 這是我們自己傳遞的,傳遞什么就是什么

girl.f()  # <__main__.A object at 0x000001F0FFE81F10>
# 但是girl.f()就不一樣了,首先girl.f()表示先通過girl獲取f對應值, 壓入運行時棧, 然后再進行調用、完事之后將返回值設置在棧頂
# 而在girl.f的時候就已經通過func_descr_get(Girl.f, girl, Girl)將這個函數和調用者綁定在一起了
# 然后調用的時候自動將調用者作為第一個參數傳遞進去

print(Girl.f)  # <function A.f at 0x000001F0FFEEFF70>
print(girl.f)  # <bound method A.f of <__main__.A object at 0x000001F0FFE81F10>>

注意:我們上面一直說的是調用者(其實說調用者也不是很准確),而不是實例對象,這是因為函數不僅可以和實例綁定,也可以和類綁定。

class Girl(object):

    @classmethod
    def f(self):
        print(self)


print(Girl.f)  # <bound method Girl.f of <class '__main__.Girl'>>
print(Girl().f)  # <bound method Girl.f of <class '__main__.Girl'>>

Girl.f()  # <class '__main__.Girl'>
Girl().f()  # <class '__main__.Girl'>

我們看到此時通過類去調用得到的不再是一個函數,而是一個方法,這是因為我們加上classmethod裝飾器,當然classmethod也是一個描述符。當類在調用的時候,類也和函數綁定起來了,因此也會得到一個方法。不過被classmethod裝飾之后,即使是實例調用,第一個參數傳遞的還是類本身,因為和 PyFunctionObject 綁定的是類、而不是實例。

所以得到的究竟是函數還是方法,就看這個函數有沒有和某個對象進行綁定,只要綁定了,那么它就會變成方法。

千變萬化的descriptor

當我們調用instance對象的函數時,最關鍵的一個動作就是從PyFunctionObject對象向PyMethodObject對象的轉變,而這個關鍵的轉變就取決於Python中的descriptor。當我們訪問對象中的屬性時,由於descriptor的存在,這種轉換自然而然的就發生了。事實上,Python中的descriptor很強大,我們可以使用它做很多事情,而在Python的內部,也存在各種各樣的descriptor,比如property、staticmethod、classmethod等等,這些descriptor給python的類機制賦予了強大的力量。具體源碼就不分析了,我們直接通過Python代碼的層面演示一下,這三種描述符的實現。

實現property

class Property:

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            # 如果instance是None說明是類調用,那么直接返回這個描述符本身
            # 這個和內置property的處理方式是一樣
            return self
        res = self.func(instance)
        return res


class A:

    @Property
    def f(self):
        return "name: hanser"


a = A()
print(a.f)  # name: hanser
print(A.f)  # <__main__.Property object at 0x000001FABFE910A0>

總結:property是為了實例對象准備的,當然property支持的功能遠不止我們上面演示的這么簡單,它還可以進行set、delete,這些我們在介紹魔法方法的時候再說吧。

實現staticmethod

class StaticMethod:

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        # 靜態方法的話,類和實例都可以用
        # 類調用不會自動傳參,但是實例會自動傳遞,因此我們需要把實例調用傳來的self給扔掉

        # 做法是直接返回self.func即可,注意:self.func是A.func
        # 因此調用的時候,是類去調用的,而類調用是不會自動加上參數的。
        return self.func


class A:

    @StaticMethod
    def f():
        return "name: hanser"


a = A()
print(a.f())  # name: hanser
print(A.f())  # name: hanser

總結:staticmethod也是為了實例對象准備的,但是類也可以調用。

實現classmethod

class ClassMethod:

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        # 類方法,目的是在類調用的時候,將類本身作為第一個參數傳遞進去
        # 顯然是這里的owner

        # 返回一個閉包,然后當調用的時候,接收參數
        # 不管是誰調用,最終這個self.func都是A.func,然后手動將cls也就是owner傳遞進去
        def inner(*args, **kwargs):
            return self.func(owner, *args, **kwargs)
        # 所以在上面我們看到, 函數被classmethod裝飾之后,即使是實例調用,第一個參數傳遞的還是類本身
        return inner


class A:

    name = "hanser"

    @ClassMethod
    def f(cls):
        return f"name: {cls.name}"


a = A()
print(a.f())  # name: hanser
print(A.f())  # name: hanser

總結:classmethod是為了類對象准備的,但是實例也可以調用。

小結

這一次我們介紹了Python中實例對象的創建以及屬性訪問,下一篇我們介紹魔法方法。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM