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還可分爲以下兩種:
在Python虛擬機訪問instance對象的屬性時,descriptor的一個做用是影響Python虛擬機對屬性的選擇。從PyObject_GenericGetAttr的僞代碼能夠看出,Python虛擬機會在instance對象自身的__dict__中尋找屬性,也會在instance對象對應的class的mro列表中尋找屬性,咱們將前一種屬性稱爲instance屬性,然後一種稱爲class屬性
雖然PyObject_GenericGetAttr裏對屬性進行選擇的算法比較複雜,可是從最終的效果上,咱們能夠總結處以下的兩條規則:
這兩條規則在對屬性進行設置時仍然會被嚴格遵照,換句話說,若是執行"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對象也是一個名字空間,不過這些名字空間經過一些特殊的規則關聯在一塊兒,使得符號的搜索過程變得複雜,從而實現了面向對象這種編程模式