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