zoukankan      html  css  js  c++  java
  • Python虚拟机类机制之从class对象到instance对象(五)

    从class对象到instance对象

    现在,我们来看看如何通过class对象,创建instance对象

    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所示

    图1-1   创建instance对象后的local名字空间

    在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对象

    这里需要特别注意,在创建<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_new

    在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字节的内存空间

    在申请了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呢?这个会在下章解答

  • 相关阅读:
    【Python】python3.6中实现同一行动态输出
    【Python】将python3.6软件的py文件打包成exe程序
    【Java】分布式RPC通信框架Apache Thrift 使用总结
    【Java】加载驱动方法
    【Java】Comparable和Comparator接口的区别
    【Java】接口(interface)VS抽象类
    【Ubuntu】PHP环境安装-phpstudy for linux版
    【Ubuntu】xrdp完美实现Windows远程访问Ubuntu 16.04
    【系统设计】会议室预定系统房间预定系统设计
    jmeter场景设计:业务占比
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9632668.html
Copyright © 2011-2022 走看看