• Python虚拟机类机制之instance对象(六)


     instance对象中的__dict__

    Python虚拟机类机制之从class对象到instance对象(五)这一章中最后的属性访问算法中,我们看到“a.__dict__”这样的形式。

    # 首先寻找'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__

    图1-1中,虚线画的dict对象就是我们期望中的a.__dict__。这个猜想可以在PyObject_GenericGetAttr中与上述的伪代码得到证实:

    object.c

    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__最初被创建的地方:

    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对象的那段代码

    再论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对象也是一个名字空间,不过这些名字空间通过一些特殊的规则关联在一起,使得符号的搜索过程变得复杂,从而实现了面向对象这种编程模式 

  • 相关阅读:
    20款时尚的 WordPress 博客主题【免费下载】
    垂涎欲滴!30个美味的食品类移动应用程序【上篇】
    Skippr – 轻量、快速的 jQuery 幻灯片插件
    Boba.js – 用于 Google 统计分析 JavaScript 库
    长期这么做的后果就是人民劳苦而得不到该有的回报,怎么能不垮
    左值与右值的根本区别在于能否获取内存地址,而能否赋值不是区分的依据
    百度后端C++电话一面
    Web 开发和数据科学家仍是 Python 开发的两大主力
    Consul架构
    去除两端逗号-JS
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9643159.html
Copyright © 2020-2023  润新知