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

posted @ 2018-09-13 21:09  北洛  阅读(711)  评论(0编辑  收藏  举报