从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虚拟机类机制之从class对象到instance对象(五)的更多相关文章

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

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

  2. Python虚拟机类机制之绑定方法和非绑定方法(七)

    Bound Method和Unbound Method 在Python中,当对作为属性的函数进行引用时,会有两种形式,一种称为Bound Method,这种形式是通过类的实例对象进行属性引用,而另一种 ...

  3. Python虚拟机类机制之descriptor(三)

    从slot到descriptor 在Python虚拟机类机制之填充tp_dict(二)这一章的末尾,我们介绍了slot,slot包含了很多关于一个操作的信息,但是很可惜,在tp_dict中,与__ge ...

  4. Python虚拟机类机制之填充tp_dict(二)

    填充tp_dict 在Python虚拟机类机制之对象模型(一)这一章中,我们介绍了Python的内置类型type如果要完成到class对象的转变,有一个重要的步骤就是填充tp_dict对象,这是一个极 ...

  5. Python虚拟机类机制之对象模型(一)

    Python对象模型 在Python2.2之前,Python中存在着一个巨大的裂缝,就是Python的内置类type,比如:int和dict,这些内置类与程序员在Python中自定义的类并不是同一级别 ...

  6. Python虚拟机类机制之自定义class(四)

    用户自定义class 在本章中,我们将研究对用户自定义class的剖析,在demo1.py中,我们将研究单个class的实现,所以在这里并没有关于继承及多态的讨论.然而在demo1.py中,我们看到了 ...

  7. Python虚拟机函数机制之位置参数的默认值(五)

    位置参数的默认值 在Python中,允许函数的参数有默认值.假如函数f的参数value的默认值是1,在我们调用函数时,如果传递了value参数,那么f调用时value的值即为我们传递的值,如果调用时没 ...

  8. Python虚拟机函数机制之扩展位置参数和扩展键参数(六)

    扩展位置参数和扩展键参数 在Python虚拟机函数机制之参数类别(三)的例3和例4中,我们看到了使用扩展位置参数和扩展键参数时指示参数个数的变量的值.在那里,我们发现在函数内部没有使用局部变量时,co ...

  9. Python虚拟机函数机制之参数类别(三)

    参数类别 我们在Python虚拟机函数机制之无参调用(一)和Python虚拟机函数机制之名字空间(二)这两个章节中,分别PyFunctionObject对象和函数执行时的名字空间.本章,我们来剖析一下 ...

随机推荐

  1. 使用Set去除String中的重复字符

    使用Set去除String中的重复字符 public class test14 { public static void main(String[] args) { String str = &quo ...

  2. JSP中,EL表达式向session中取出一个attribute和JSP脚本访问session取出一个attribute,写法有何不同?(转自百度知道)

    EL表达式使用起来会更简洁,假如session中有一个属性A(attrA),那么EL和jsp脚本取值的方式如下: EL表达式:${ sessionScope.attrA } JSP脚本:<%=s ...

  3. js中的位运算符 ,按位操作符

    按位操作符(Bitwise operators) 将其操作数(operands)当作32位的比特序列(由0和1组成),而不是十进制.十六进制或八进制数值.例如,十进制数9,用二进制表示则为1001.按 ...

  4. weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架

    写在前面的话: weex官方文档不完善,在整个实施过程中遇到过很多坑,中途几次想放弃,总是有些不甘心.攻坚克难,总也是会有一些收获,先将收获进行分享也或是记录,防止忘记.要想用好weex必须对es5/ ...

  5. Vue.js(2.x)之列表渲染(v-for/key)

    1.v-for是Vue里的循环语句,与其他语言的循环大同小异.首先得有需要循环且不为空的数组,循环的关键字为in或of. 需要索引时的写法: v-for里的in可以使用of代替: 还可以使用v-for ...

  6. Android中渐变图片失真的解决方案

    在android开发(尤其是android游戏开发)中有一个很严重的问题就是带有渐变效果的png图片会出现严重的banding(色带),鉴于这种情况,有几种可行的解决方法:   1.如果Activit ...

  7. uLua学习之创建游戏对象(二)

    前言 上节,刚刚说到创建一个“HelloWorld”程序,大家想必都对uLua有所了解了,现在我们一步步地深入学习.在有关uLua的介绍中(在这里),我们可以发现它使用的框架是Lua + LuaJIT ...

  8. .NET中异常类(Exception)

    异常:程序在运行期间发生的错误.异常对象就是封装这些错误的对象. try{}catch{}是非常重要的,捕获try程序块中所有发生的异常,如果没有捕获异常的话,程序运行的线程将会挂掉,更严重的是这些错 ...

  9. Oracle数据库基础--建表语法+操作

    语法 1.建表 create table 表名( 列名 数据类型, …… ); 2.删除表:drop table 表名; 3.添加列:alter table 表名 add(列名 数据类型); 4.修改 ...

  10. 远程链接mongoDB robomongo

    墙裂推荐一个软件robomongo 下载地址:https://robomongo.org/download 最初不用这个软件的时候需要shell链接mongoDB,折腾了半天结果版本不匹配 用robo ...