Python虛擬機類機制之instance對象(六)

 instance對象中的__dict__html

Python虛擬機類機制之從class對象到instance對象(五)這一章中最後的屬性訪問算法中,咱們看到「a.__dict__」這樣的形式。python

# 首先尋找'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>建立<instance a>時,Python虛擬機僅爲a申請了16個字節的內存,並無額外建立PyDictObject對象的動做。不過在<instance a>中,24個字節的前8個字節是PyObject,後8個字節是爲兩個PyObject *申請的,難道謎底就在這多出的兩個PyObject *?算法

在建立<class A>時,咱們曾說到,Python虛擬機設置了一個名爲tp_dictoffset的域,從名字上判斷,這個可能就是instance對象中__dict__的偏移位置。下圖1-1展現了咱們的猜測:編程

圖1-1   猜測中的a.__dict__app

圖1-1中,虛線畫的dict對象就是咱們指望中的a.__dict__。這個猜測能夠在PyObject_GenericGetAttr中與上述的僞代碼獲得證明:函數

object.cpost

PyObject * PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
	PyTypeObject *tp = obj->ob_type;
	PyObject *descr = NULL;
	PyObject *res = NULL;
	descrgetfunc f;
	Py_ssize_t dictoffset;
	PyObject **dictptr;
	……
	dictoffset = tp->tp_dictoffset;
	if (dictoffset != 0) {
		PyObject *dict;
		//處理變長對象
		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);

			dictoffset += (long)size;
			assert(dictoffset > 0);
			assert(dictoffset % SIZEOF_VOID_P == 0);
		}
		dictptr = (PyObject **) ((char *)obj + dictoffset);
		dict = *dictptr;
		if (dict != NULL) {
			Py_INCREF(dict);
			res = PyDict_GetItem(dict, name);
			……
		}
	}
	……
}

  

若是dictoffset小於0,意味着A是繼承自str這樣的變長對象,Python虛擬機會對dictoffset進行一些處理,最終仍然會使dictoffset指向a的內存額外申請的位置。而PyObject_GenericGetAttr正是根據這個dictoffset得到一個dict對象。更進一步,查看函數g中有設置self(即<instance a>)中設置的a屬性,這個instance對象的屬性設置動做也會訪問a.__dict__,並且這個動做最終調用的PyObject_GenericSetAttr也是a.__dict__最初被建立的地方:spa

object.c指針

int PyObject_GenericSetAttr(PyObject *obj, PyObject *name, PyObject *value)
{
	PyTypeObject *tp = obj->ob_type;
	PyObject *descr;
	……
	dictptr = _PyObject_GetDictPtr(obj);
	if (dictptr != NULL) {
		PyObject *dict = *dictptr;
		if (dict == NULL && value != NULL) {
			dict = PyDict_New();
			if (dict == NULL)
				goto done;
			*dictptr = dict;
		}
		……
	}
	……
}

  

其中_PyObject_GetDictPtr的代碼就是PyObject_GenericGetAttr中根據dictoffset得到dict對象的那段代碼htm

再論descriptor

在上面的僞代碼中出現了「descriptor」,這個命名實際上是有意爲之,目的是喚起前面咱們在Python虛擬機類機制之填充tp_dict(二)這一章中所描述過的descriptor。前面咱們看到,在PyType_Ready中,Python虛擬機會填充tp_dict,其中與操做名對應的是一個個descriptor,那時咱們看到的是descriptor這個概念在Python內部是如何實現的。如今,咱們將要剖析的是descriptor在Python的類機制究竟會起到怎樣的做用

在Python虛擬機對class對象或instance對象進行屬性訪問時,descriptor將對屬性訪問的行爲產生重大的影響,通常而言,對於一個Python中的對象obj,若是obj.__class__對應的class對象中存在__get__、__set__和__delete__三種操做,那麼obj就能夠稱爲Python的一個descriptor。在slotdefs中,咱們會看到__get__、__set__、__delete__對應的操做:

typeobject.c

static slotdef slotdefs[] = {
……
TPSLOT("__get__", tp_descr_get, slot_tp_descr_get, wrap_descr_get,
	   "descr.__get__(obj[, type]) -> value"),
TPSLOT("__set__", tp_descr_set, slot_tp_descr_set, wrap_descr_set,
	   "descr.__set__(obj, value)"),
TPSLOT("__delete__", tp_descr_set, slot_tp_descr_set,
	   wrap_descr_delete, "descr.__delete__(obj)"), 
……
}

  

在前面幾章咱們看到了PyWrapperDescrObject、PyMethodDescrObject等對象,它們對應的class對象中分別爲tp_descr_get設置了wrapperdescr_get、method_get等函數,因此,它們是descriptor

若是細分,那麼descriptor還可分爲以下兩種:

  • data descriptor:type中定義了__get__和__set__的descriptor
  • non data descriptor:type中只定義了__get__的descriptor

在Python虛擬機訪問instance對象的屬性時,descriptor的一個做用是影響Python虛擬機對屬性的選擇。從PyObject_GenericGetAttr的僞代碼能夠看出,Python虛擬機會在instance對象自身的__dict__中尋找屬性,也會在instance對象對應的class的mro列表中尋找屬性,咱們將前一種屬性稱爲instance屬性,然後一種稱爲class屬性

雖然PyObject_GenericGetAttr裏對屬性進行選擇的算法比較複雜,可是從最終的效果上,咱們能夠總結處以下的兩條規則:

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

這兩條規則在對屬性進行設置時仍然會被嚴格遵照,換句話說,若是執行"a.value = 1",就算在A中發現一個名爲"value"的no data descriptor,那麼仍是會設置a.__dict__['value'] = 1,而不會設置A中已有的屬性

當最終得到的屬性是一個descriptor,最神奇的事發生了,Python虛擬機不是簡單的返回descriptor,而是如僞代碼所示的那樣,調用descriptor.__get__,將調用的結果返回,在下面的代碼示例中,展現了descriptor對屬性訪問行爲的影響:

descriptor改變返回值

>>> class A(list):
...     def __get__(self, instance, owner):
...         return "A.__get__"
...
>>> class B(object):
...     value = A()
...
>>> b = B()
>>> b.value
'A.__get__'
>>> s = b.value
>>> type(s)
<class 'str'>

  

instance屬性優先於non data descriptor

>>> class A(list):
...     def __get__(self, instance, owner):
...         return "A.__get__"
...
>>> class B(object):
...     value = A()
...
>>> b = B()
>>> b.value = 1
>>> b.__dict__["value"]
1
>>> b.__class__.__dict__["value"]
[]

  

data descriptor優先於instance屬性

>>> class A(list):
...     def __get__(self, instance, owner):
...         return "A.__get__"
...     def __set__(self, instance, value):
...         print("A.__set__")
...         self.append(value)
...
>>> class B(object):
...     value = A()
...
>>> b = B()
>>> b.value = 1
A.__set__
>>> b.__dict__["value"]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'value'
>>> b.__class__.__dict__["value"]
[1]

  

前面咱們說,當訪問的屬性最終對應的是一個descriptor時,會調用其__get__方法,並將__get__的結果做爲返回。其實這個說法不是徹底正確的,仔細對比type_getattro和PyObject_GenericGetAttr的代碼,咱們會發現它們在對待descriptor上存在差別。在PyObject_GenericGetAttr中,若是查詢到的descriptor存在於class對象的tp_dict中,會調用其__get__方法;若它存在於instance對象的tp_dict中,則不會調用其__get__方法

>>> class A(object):
...     def __get__(self, instance, owner):
...         return "Python"
...
>>> class B(object):
...     desc_in_class = A()
...
>>> B.desc_in_class
'Python'
>>> b = B()
>>> b.desc_in_class
'Python'
>>> b.desc_in_class = A()
>>> b.desc_in_class
<__main__.A object at 0x000000FBDD76C908>

  

到這裏,咱們已經看到,descriptor對屬性訪問的影響主要在兩個方面:其一是對訪問順序的影響,其二是對訪問結果的影響,第二種影響正是類的成員函數調用的關鍵

函數變身

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)

  

在前面討論建立class A對象時,咱們看到A.__dict__中保存了一個與符號"f"對應的PyFunctionObject對象,因此在僞代碼中的descriptor對應的就是一個PyFunctionObject對象。先拋開僞代碼中肯定最終返回值的過程不說,咱們從另外一個角度來看一看,假設PyFunctionObject做爲LOAD_ATTR的最終結果,在LOAD_ATTR指令代碼的最後被SET_TOP壓入到運行時棧,那會有什麼後果呢?

在A的成員函數f的def語句中,咱們看到一個self參數,self在Python中是否是一個有效的參數呢?仍是它僅僅是語法意義上的佔位符?這一點能夠從g中看到答案,在函數g中有再這樣的語句:self.value = aValue。這條語句毫無疑問地揭示了self是一個貨真價實的參數,因此也代表了函數f也是一個帶參函數。如今,問題來了,根據咱們以前對函數機制的分析,Python一般會將參數事先壓入運行時棧中,可是demo1.py中的a.f語句編譯後的指令序列中能夠看到,Python在得到a.f對應的對象後,沒有進行任何普通函數調用時將參數壓入棧的動做,而是直接執行了CALL_FUNCTION指令

a.f()調用指令

16       31 LOAD_NAME                2 (a)
		 34 LOAD_ATTR                3 (f)
		 37 CALL_FUNCTION            0
		 40 POP_TOP  

  

這裏沒有任何像參數的東西在棧中,棧中只有一個多是a.f的PyFunctionObject對象,那麼這個遺失的self參數究竟在什麼地方?

既然棧中沒有參數,而棧中惟一的PyFunctionObject對象又須要參數,那麼說明,咱們以前的推理多是錯誤的,因此,棧中的對象只能是另外一種咱們還沒有告終的對象,因爲是經過訪問屬性"f"獲得的這個對象,因此一個合理的假設是:在這個對象中,還包含函數f的參數:self

在以前介紹函數機制的時候,咱們彷佛忘記介紹一個對象PyFunction_Type,這是PyFunctionObject對象對應的class對象,觀察PyFunction_Type對象,咱們會發現與__get__對應的tp_descr_get被設置爲&func_descr_get,這意味着這裏的A.f其實是一個descriptor。因爲PyFunc_Type中並無設置func_descr_set,因此A.f是一個non data descriptor。此外,因爲在a.__dict__中沒有f符號的存在,因此根據僞代碼中的算法,a.f的的返回值將被descriptor改變,其結果將是A.f.__get__,也就是func_descr_get(A.f, a, A)

funcobject.c

PyTypeObject PyFunction_Type = {
	……
	func_descr_get,				/* tp_descr_get */
	……
};

……
static PyObject *
func_descr_get(PyObject *func, PyObject *obj, PyObject *type)
{
	if (obj == Py_None)
		obj = NULL;
	return PyMethod_New(func, obj, type);
}

  

func_descr_get將A.f對應的PyFunctionObject進行了一番包裝,經過PyMethod_New在PyFunctionObject的基礎上建立了一個新的對象,因而,咱們再進入到PyMethod_New

funcobject.c

PyObject * PyMethod_New(PyObject *func, PyObject *self, PyObject *klass)
{
	register PyMethodObject *im;
	……
	im = free_list;
	if (im != NULL) {
		//使用緩衝池
		free_list = (PyMethodObject *)(im->im_self);
		PyObject_INIT(im, &PyMethod_Type);
	}
	else {
		//不使用緩衝池,直接建立PyMethodObject對象
		im = PyObject_GC_New(PyMethodObject, &PyMethod_Type);
		if (im == NULL)
			return NULL;
	}
	im->im_weakreflist = NULL;
	Py_INCREF(func);
	im->im_func = func;
	Py_XINCREF(self);
	//這裏就是self對象
	im->im_self = self;
	Py_XINCREF(klass);
	im->im_class = klass;
	_PyObject_GC_TRACK(im);
	return (PyObject *)im;
}

  

這裏咱們能夠知道,原先運行時棧中已經再也不是PyFunctionObject對象,而是PyMethodObject對象。看到free_list這樣熟悉的字眼,咱們能夠當即判斷出,在PyMethodObject的實現和管理中,Python採用了緩衝池的技術,如今來看一看這個PyMethodObject

typedef struct {
    PyObject_HEAD
    PyObject *im_func;   //可調用的PyFunctionObject對象
    PyObject *im_self;   //用於成員函數調用的self參數,instance對象
    PyObject *im_class;  //class對象
    PyObject *im_weakreflist; 
} PyMethodObject;

  

在PyMethod_New中,分別將im_func、im_self、im_class設置了不一樣的值,結合a.f,分別對應符號"f"所對應的PyFunctionObject對象,符號"a"對應的instance對象,以及<class A>對象

在Python中,將PyFunctionObject對象和一個instance對象經過PyMethodObject對象結合在一塊兒的過程就稱爲成員函數的綁定。下面的代碼清晰地展現了在訪問屬性時,發生函數綁定的結果:

>>> class A(object):
...     def f(self):
...         pass
...
>>> a = A()
>>> a.__class__.__dict__["f"]
<function A.f at 0x000000FBDD74E620>
>>> a.f
<bound method A.f of <__main__.A object at 0x000000FBDD76CE80>>

  

無參函數的調用

在LOAD_ATTR指令以後,指令"37   CALL_FUNCTION   0"開始了函數調用的動做,以前咱們研究過對於PyFunctionObject對象的調用,而對於PyMethodObject對象,狀況則有些不一樣,以下:

ceval.c

static PyObject * call_function(PyObject ***pp_stack, int oparg)
{
	int na = oparg & 0xff;
	int nk = (oparg >> 8) & 0xff;
	int n = na + 2 * nk;
	PyObject **pfunc = (*pp_stack) - n - 1;
	PyObject *func = *pfunc;
	PyObject *x, *w;

	……
	if (PyCFunction_Check(func) && nk == 0)
	{
		……
	}
	else
	{	//[1]:從PyMethodObject對象中抽取PyFunctionObject對象和self參數
		if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL)
		{
			PyObject *self = PyMethod_GET_SELF(func);
			func = PyMethod_GET_FUNCTION(func);
			//[2]:self參數入棧,調整參數信息變量
			*pfunc = self;
			na++;
			n++;
		}
		if (PyFunction_Check(func))
			x = fast_function(func, pp_stack, n, na, nk);
		else
			x = do_call(func, pp_stack, na, nk);
		……
	}
	……
	return x;
}

  

調用成員函數f時,顯示傳入的參數個數爲0,也就是說,調用f時,Python虛擬機沒有進行參數入棧的動做。而f顯然至少須要一個實例對象的參數,而正是在call_function中,Python虛擬機爲PyMethodObject進行了一些參數處理的動做

Python虛擬機執行a.f()時,在call_function中,代碼[1]處的判斷將會成立,其中PyMethod_GET_SELF被定義爲:

classobject.h

#define PyMethod_GET_SELF(meth) \
	(((PyMethodObject *)meth) -> im_self)

  

在call_function中,func變量指向一個PyMethodObject對象,在上述代碼[1]處成立後,在if分支中又會將PyMethodObject對象中的PyFunctionObject對象和instance對象分別提取出來,在if分支中有一處最重要的代碼,即[2]處,pfunc指向的位置正是運行時棧中存放PyMethodObject對象的位置,那麼這個原本屬於PyMethodObject對象的地方改成存放instance對象究竟有什麼做用呢?在這裏,Python虛擬機以另外一種方式完成了函數參數入棧的動做,原本屬於PyMethodObject對象的內存空間被用做了函數f的self參數的容身之處,圖1-1展現了運行call_function時運行時棧的變化狀況:

圖1-1   設置self參數

a是設置pfunc以前的運行時棧,b表示設置了pfunc以後的運行時棧。在call_function中,接着還會經過PyMethod_GET_FUNCTION將PyMethodObject對象中的PyFunctionObject對象取出,隨後在[2]處,Python虛擬機完成了self參數的入棧,同時還調整了維護着參數信息的na和n,調整後的結果意味着函數會得到一個位置參數,看一看class A中的f的def語句,self正是一個位置參數

因爲func在if分支以後指向了PyFunctionObject對象,因此接下來Python執行引擎將進入fast_function。到了這裏,剩下的動做就和咱們以前所分析的帶參函數的調用一致。實際上a.f的調用是指上就是一個帶一個位置參數的通常函數調用,而在fast_function,做爲self參數的<instance a>被Python虛擬機壓入到了運行時棧中,因爲a.f僅僅是一個帶位置參數的函數,因此Python執行引擎將進入快速通道,在快速通道中,運行時棧中的這個instance對象會被拷貝到新的PyFrameObject對象的f_localsplus中

ceval.c

static PyObject * fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
	PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
	PyObject *globals = PyFunction_GET_GLOBALS(func);
	PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
	PyObject **d = NULL;
	int nd = 0;

	PCALL(PCALL_FUNCTION);
	PCALL(PCALL_FAST_FUNCTION);
	if (argdefs == NULL && co->co_argcount == n && nk == 0 &&
		co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE))
	{
		//建立新的PyFrameObject對象f
		PyFrameObject *f;
		f = PyFrame_New(tstate, co, globals, NULL);
		if (f == NULL)
			return NULL;

		fastlocals = f->f_localsplus;
		//[1]:得到棧頂指針
		stack = (*pp_stack) - n;

		for (i = 0; i < n; i++)
		{	
			//[2]:
			fastlocals[i] = *stack++;
		}
		……
	}
	……
}

  

在調用fast_function時,參數的數量n已經由執行CALL_FUNCTION時的0變爲了1,因此代碼[1]處的stack指向的位置就和圖1-1中pfunc指向的位置是一致的了,在代碼的[2]處將<instance a>做爲參數拷貝到函數的參數區fastlocals中,必須將它放置到棧頂,也就是之前PyMethodObject對象所在的位置上,也也就是前面call_function那個賦值操做的緣由

帶參函數的調用

Python虛擬機對類中帶參的成員函數的調用,其原理和流程與無參函數的調用是一致的,咱們來看看a.g(10)的字節碼序列: 

17       41 LOAD_NAME                2 (a)
		 44 LOAD_ATTR                4 (g)
		 47 LOAD_CONST               2 (10)
		 50 CALL_FUNCTION            1
		 53 POP_TOP   

  

能夠看到,和調用成員函數f的指令序列幾乎徹底一致,只是多了一個"47   LOAD_CONST   2 (10)"。對於這個指令咱們不會陌生,在分析函數機制的時候,咱們看到它是用來將函數所需的參數壓入到運行時棧中。對於g,真正有趣的地方在於考察函數的實現代碼,從而能夠看到那個做爲self參數的instance對象的使用: 

>>> dis.dis(A.g)
 11           0 LOAD_FAST                1 (aValue)
              3 LOAD_FAST                0 (self)
              6 STORE_ATTR               0 (value)

 12           9 LOAD_FAST                0 (self)
             12 LOAD_ATTR                0 (value)
             15 PRINT_ITEM          
             16 PRINT_NEWLINE     

  

顯然,其中的LOAD_FAST、LOAD_ATTR、STORE_ATTR這些字節碼指令都涉及到了做爲self參數的instance對象,有興趣的同窗能夠分析一下STORE_ATTR的代碼,能夠發現其中也有相似於LOAD_ATTR中PyObject_GenericGetAttr的屬性訪問算法

其實到了這裏,咱們能夠在更高的層次俯視一下Python的運行模型,最核心的模型其實很是簡單,能夠簡化爲兩條規則:

  • 在某個名字空間中尋找符號對應的對象
  • 對從名字空間中獲得的對象進行某些操做

拋開面向對象花裏胡哨的外表,其實咱們會發現,class對象其實就是一個名字空間,instance對象也是一個名字空間,不過這些名字空間經過一些特殊的規則關聯在一塊兒,使得符號的搜索過程變得複雜,從而實現了面向對象這種編程模式 

相關文章
相關標籤/搜索