Python虛擬機類機制之從class對象到instance對象(五)

從class對象到instance對象html

如今,咱們來看看如何經過class對象,建立instance對象python

demo1.py算法

class A(object):
    name = "Python"
 
    def __init__(self):
        print("A::__init__")
 
    def f(self):
        print("A::f")
 
    def g(self, aValue):
        self.value = aValue
        print(self.value)
 
 
a = A()
a.f()
a.g(10)

  

Python虛擬機類機制之自定義class(四)這一章中,咱們看到了Python虛擬機是如何執行class A語句的,如今,咱們來看看,當咱們實例化一個A對象,Python虛擬機又是如何執行的函數

a = A()
//字節碼指令
22  LOAD_NAME                1 (A)
25  CALL_FUNCTION            0
28  STORE_NAME               2 (a)

  

在前面一節Python虛擬機類機制之自定義class(四),咱們看到在建立class對象的最後,Python執行引擎經過STORE_NAME指令,將建立好的class對象放入到local名字空間,因此在實例化class A的時候,指令"22   LOAD_NAME   1 (A)"會從新將class A對象取出,壓入到運行時棧中。以後,又是經過一個CALL_FUNCTION指令來建立instance對象。在建立完instance對象以後,再次經過STORE_NAME指令將實例對象a放入到local名字空間中。因此,這段字節碼指令序列完成以後,local名字空間如圖1-1所示post

圖1-1   建立instance對象後的local名字空間spa

在CALL_FUNCTION中,Python一樣會沿着call_function->do_call->PyObject_Call的調用路徑進入到PyObject_Call中。前面說過,所謂「調用」,就是執行對象的type所對應的class對象的tp_call操做。因此,在PyObject_Call中,Python執行引擎會尋找class對象<class A>的type中定義的tp_call操做。<class A>的type爲<type 'type'>,因此,最終將調用tp_call,在PyType_Type.tp_call中又調用了A.tp_new是用來建立instance對象3d

這裏須要特別注意,在建立<class A>這個class對象時,Python虛擬機調用PyType_Ready對<class A>進行了初始化,其中的一項動做就是繼承基類的操做,因此A.tp_new會繼承自object.tp_new。在PyBaseObject_Type中,這個操做被定義爲object_new。建立class對象和建立instance對象的不一樣之處正是在於tp_new不一樣,建立class對象,Python虛擬機使用的是tp_new,而對於instance對象,Python虛擬機使用的object_neworm

在object_new中,調用了A.tp_alloc,這個操做也是從object繼承而來的,是PyType_GenericAlloc。前面咱們提到,PyType_GenericAlloc最終將申請A.tp_basicsize+A.tp_itemsize大小的內存空間。上一節,這兩個量的計算結果爲A.tp_basicsize=PyBaseObject_Type.tp_basicsize+8=sizeof(PyObject)+8=24;A.tp_itemsize=PyBaseObject_Type.tp_itemsize=0。原來,object_new的全部工做就是申請一個24字節的內存空間htm

在申請了24字節的內存空間,回到type_call以後,因爲建立的不是class對象,而是instance對象,type_call會嘗試進行初始化的動做對象

typeobject.c

static PyObject * type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    PyObject *obj;
 
    obj = type->tp_new(type, args, kwds);
    if (obj != NULL) {
        if (type == &PyType_Type &&
            PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&
            (kwds == NULL ||
             (PyDict_Check(kwds) && PyDict_Size(kwds) == 0)))
            return obj;
        if (!PyType_IsSubtype(obj->ob_type, type))
            return obj;
        type = obj->ob_type;
        if (PyType_HasFeature(type, Py_TPFLAGS_HAVE_CLASS) &&
            type->tp_init != NULL &&
            type->tp_init(obj, args, kwds) < 0) {
            Py_DECREF(obj);
            obj = NULL;
        }
    }
    return obj;
}

  

基於<class A>建立的instance對象obj,其ob_type固然也在PyType_GenericAlloc中被設置爲指向<class A>,其tp_init在PyType_Ready時會繼承PyBaseObject_Type的object_init操做,由於A的定義中重寫了__init__,因此在fix_slot_dispatchers中,tp_init會指向slotdefs中指定的__init__對應的slot_tp_init

typeobject.c

static int slot_tp_init(PyObject *self, PyObject *args, PyObject *kwds)
{
    static PyObject *init_str;
    PyObject *meth = lookup_method(self, "__init__", &init_str);
    PyObject *res;
 
    if (meth == NULL)
        return -1;
    res = PyObject_Call(meth, args, kwds);
    Py_DECREF(meth);
    if (res == NULL)
        return -1;
    if (res != Py_None) {
        PyErr_Format(PyExc_TypeError,
                 "__init__() should return None, not '%.200s'",
                 res->ob_type->tp_name);
        Py_DECREF(res);
        return -1;
    }
    Py_DECREF(res);
    return 0;
}

  

在執行slot_tp_init時,Python虛擬機會首先經過lookup_method在class對象及其mro列表中搜索屬性__init__對應的操做,而後經過PyObject_Call調用該操做。在定義class時,重寫__init__操做,那麼搜索的結果就是咱們寫的操做,若是沒有重寫,那麼最終的結果將是調用object._init,在object_init中,Python虛擬機什麼也不作,直接返回,因此,當咱們用a = A()建立一個instance對象時,實際上沒有進行任何初始化的動做

到這裏,咱們稍微小結一下從class對象到instance對象的兩個步驟:

  • instance = class.__new__(class, args, kwds)
  • class.__init__(instance, args, kwds)

其中,args爲一個tupple對象,裏面包含着建立instance對象的各個參數,而kwds一般爲NULL。須要注意的是,這兩個步驟也適用於從metaclass對象建立class對象。從metaclass對象建立class對象的過程也是從一個從class對象建立instance對象

訪問instance對象中的屬性

在Python中,形如x.y或x.y()形式的表達式稱爲「屬性引用」,其中x爲對象,y爲對象的屬性。這個屬性,有可能只是簡單的數據,好比字符串或整數,也有多是成員函數這類比較複雜的東西。在class A中一共有兩個函數,一個是不須要參數的成員函數,一個是須要參數的成員函數,這裏,咱們先來看看,對於不須要參數的成員函數,其調用過程是怎樣的

a.f()
//字節碼指令
31  LOAD_NAME                2 (a)
34  LOAD_ATTR                3 (f)
37  CALL_FUNCTION            0
40  POP_TOP

  

Python虛擬機經過指令LOAD_NAME會將local名字空間與符號a對應的instance對象壓入運行時棧中,隨後執行指令"34   LOAD_ATTR   3"是屬性訪問機制的關鍵所在,它會從<instance a>中得到與符號f對應的對象,這是個PyFunctionObject對象

ceval.c

case LOAD_ATTR:
    w = GETITEM(names, oparg);
    v = TOP();
    x = PyObject_GetAttr(v, w);
    Py_DECREF(v);
    SET_TOP(x);
    if (x != NULL)
        continue;
    break;

  

其中,w爲PyStringObject對象f,而v爲運行時棧中的那個instance對象<instance a>,從<instance a>中得到f對應對象的關鍵就在PyObject_GetAttr中

object.c

PyObject * PyObject_GetAttr(PyObject *v, PyObject *name)
{
    PyTypeObject *tp = v->ob_type;
    //[1]:經過tp_getattro得到屬性對應對象
    if (tp->tp_getattro != NULL)
        return (*tp->tp_getattro)(v, name);
    //[2]:經過tp_getattr得到屬性對應對象
    if (tp->tp_getattr != NULL)
        return (*tp->tp_getattr)(v, PyString_AS_STRING(name));
    //[3]:屬性不存在,拋出異常
    PyErr_Format(PyExc_AttributeError,
             "'%.50s' object has no attribute '%.400s'",
             tp->tp_name, PyString_AS_STRING(name));
    return NULL;
}

  

在Python的class對象中,定義了兩個與訪問屬性相關的操做:tp_getattro和tp_getattr。其中的tp_getattro是首選的屬性訪問操做,而tp_getattr在Python中已再也不推薦使用,它們之間的區別主要是在屬性名的使用上,tp_getattro所使用的屬性名必須是一個PyStringObject對象,而tp_attr所使用的屬性名必須是一個C中的原生字符串。若是某個類型同時定義了tp_getattr和tp_getattro兩種屬性訪問操做,那麼PyObject_GetAttr將優先使用tp_getattro操做

在Python虛擬機建立<class A>時,會從PyBaseObject_Type中繼承tp_getattro——PyObject_GenericGetAttr,因此Python虛擬機在這裏會進入PyObject_GenericGetAttr。在PyObject_GenericGetAttr中,有一套複雜地肯定訪問屬性的算法,下面以a.f爲例,咱們用僞代碼看一下是如何肯定這個屬性的

# 首先尋找'f'對應的descriptor(descriptor在以後會細緻剖析)
# 注意:hasattr會在<class A>的mro列表中尋找符號'f'
if hasattr(A, 'f'):
    descriptor = A.f
type = descriptor.__class__
if hasattr(type, '__get__') and (hasattr(type, '__set__') or 'f' not in a.__dict__):
    return type.__get__(descriptor, a, A)

# 經過descriptor訪問失敗,在instance對象自身__dict__中尋找屬性
if 'f' in a.__dict__:
    return a.__dict__['f']

# instance對象的__dict__中找不到屬性,返回a的基類列表中某個基類裏定義的函數
# 注意:這裏的descriptor實際上指向了一個普通的函數
if descriptor:
    return descriptor.__get__(descriptor, a, A)

  

咱們經過一段代碼來驗證這個僞代碼的描述:

class A(object):
    def func(self):
        pass


a = A()
a.func = 1
print(a.func)

  

這段代碼很直觀,最後會輸出1,看上去與上面的僞代碼描述的不對啊。實際上,上面的僞代碼中有一個關鍵的概念——descriptor。在一個class中,並非隨意定義一個函數就是descriptor了,因此致使輸出結果爲1。那麼,究竟什麼纔是descriptor呢?這個會在下章解答

相關文章
相關標籤/搜索