今天继续学习第十二章 类体系(下)
本节学习用户自定义 type (class). 下面的示例 python 类包含类定义, 构造函数, 实例化等多个要点:
class A: # 类缺省派生自 object name = 'Python' # 属性 def __init__(self): # 这是构造函数 print 'A::__init__' def f(self): # 一般成员函数 print 'A::f' a = A() # 实例化 a.f() # 调用成员函数.
(编译后形成多个 PyCodeObject, 以及其关系略)
class 的动态元信息
这里 class 的元信息指关于 class 自身的信息, 如 class 的名字, 含有什么属性,方法, 空间大小等.
如这个 python 类 A, 含一个名字 f, 其对应一个方法(method). 根据这些元信息, 才能创建出此类的 instance.
可对应到 java/c# 中的 Class (反射)信息, python 也提供这种元信息, 并具备高度灵活的动态性.
Python 字节码相关创建类的指令有 BUILD_CLASS,MAKE_FUNCTION.
估计是使用一组指令序列创建出一个 class 实体, 然后向该 class 添加 name, __init__, f 名字->值的对.
这些值对被存入 frame 的 locals 名字空间, 也即存放的是 class A 的(动态)元信息.
此后建立好的 locals 名字空间(即包含 A 的元信息的 dict) 传递给指令 BUILD_CLASS, 对应内部函数
build_class(methos, bases, class_name):
// ceval.c PyObject *build_class(dict methods, tuple bases, string name) { ... result = PyObject_CallFunctionObjArgs(metaclass, name, bases, methods) ... }
这里 methods 参数即是前面 locals, 含新类的所有(程序员给出的)元信息, 包括属性和方法.
该函数前面计算一个 metaclass 参数, 与 classic class(旧式类) 和 new style class 有关, 历史遗留问题...
如果未指定 metaclass, 则选用第一基类(缺省是object) 的 type 作为 metaclass.
也即 <type 'type'> .
图12-21 显示多个 class 对象的静态元信息, 动态元信息的关系.
传递到函数 PyObject_CallFunctionObjArgs() 的第一个参数是 <type 'type'>, 即内部的 PyType_Type
对象, 这个对象是 callable 的, 对它进行 call 产生的效果就是构造出一个 class 对象.
// abstract.c PyObject *PyObject_Call(PyObject *func, PyObject *args ...) { // 这里 args 可认为是要新建的 class 的 name,bases,methods 打包... XXX_FuncType *call = func->ob_type->tp_call; PyObject *result = (*call)(func as this, args); // 这个返回结果应该就是新建的 class. }
也即对 Type metaclass 的调用, 就会产生一个新的 class 对象. 道理同对 class 的调用就会产生一个 instance .
这些繁杂的创建一个 Type metaclass 的 instance 的事务发生在:
// typeobject.c PyObject *type_new(PyTypeObject *metatype, PyObject *args ... ) { // 这里 metatype 即是 PyType_Type, args 含类名,基类,方法集合. // 解析变量... var name = args[name], bases = args[bases], dict = args[dict]; // 伪代码. // 申请内存, 为放置在 heap 中的 type 对象: heap-type. PyHeapTypeObject *type = metatype->tp_alloc(...); // 初始化 type 的很多域(略). type->xxx = xxx ...... // 要点: 调用 PyType_Ready() 就绪此 type(class) 对象. PyType_Ready(type); return type; // 返回新建的 class 对象. }
这下也就明白了, PyHeapTypeObject 就是专门为用户自定义的 class 对象准备的.
同时我们对 Python 中 "可调用(callable)" 概念也有一定的感性认识了. 在 python 中, 创建一个类的实例
直接用 a = A() 即将这个类作为 callable 调用即可, 与其它语言的 a = new A() 就稍稍有点差异. (和 js 像).
终于, 前面准备 class 的曲折工作完成了, 后面就是用这个 class 来创建 instance 了. 虚拟机指令示例如下:
a = A() # 创建 class A 的新实例(instance) LOAD_NAME 'A' # class 的名字, 在 globals 或 locals 名字空间中. CALL_FUNCTION # 当做 callable 调用, 创建出一个 instance. STORE_NAME 'a' # 新对象绑定到名字 'a'
也即验证了 "调用 class 对象将创建 instance 对象". (这里我们可以认为 a = new A() 是 a = A() 的语法糖).
所谓调用, 就是内部对 type->tp_call 函数的调用, 这里最终调用到 A.tp_new() 函数, 从而创建出 instance.
对于 type 是 tp_new 函数, 对于一般 class 是 object_new 函数. 后者核心工作是分配一个 PyXXXObject
所需空间(大约24字节), 然后初始化这个对象的 type=class A, 然后这个对象就是 A 的 instance 了.
对象创建之后, 会调用 tp_init, 如果对象重写 __init__ 方法, 则会调用到以实现构造语义.
(这里搜索 __init__ 方法涉及到 mro 搜索)
小结, 从 class 对象创建 instance 对象的两步骤:
1. instance = class.__new__(class, args, kwds)
2. class.__init__(instance, args, kwds)
参数 args 通常含创建参数; kwds 是键参数, 通常为 NULL.
从 metaclass 对象创建 class 对象的过程, 与从 class 对象创建 instance 的过程相同.
这样如前面所提及, class 对象具有二相性. (被别人创建, 也创建别人, 以同样的方式)
在 python 中, 可访问对象的属性如 x.y, x.f() (方法可认为是广义的属性). 访问属性,方法的字节码为:
a.f() LOAD_NAME 'a' # 访问对象 a LOAD_ATTR 'f' # 访问对象 a 的属性 f. CALL_FUNCTION # 将 f 当做函数调用, 即完成 a.f() 语义.
这里 LOAD_ATTR 即读取属性的指令, 实现为调用函数 PyObject_GetAttr(obj, attr_name).
// object.c PyObject *PyObject_GetAttr(PyObject *v, PyObject *name) { type = v->tp_type; // 对象的类型. if (type->tp_getattro) // 如果提供有这个访问函数, 则用它查找. return (*type->tp_getattro)(v, name); if (type->tp_getattr) // 如果提供 getattr 访问函数, 则用它... return (*type->tp_getattr)(v, name); // 未提供访问方法, 则抛出异常. throw new PyExc_AttributeError; }
这里 python 的首选属性访问函数为 tp_getattro, 次之的 tp_getattr 已不再推荐使用(细微区别在参数).
在创建类 A 的时候, 会从基类 object 继承 tp_getattro -- PyObject_GenericGetAttr, 所以会调用它.
而这里有一套复杂的确定访问属性的算法. 如访问 a.f 为例:
# 首先寻找 'f' 的 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'] # 还找不到 ... if descriptor: return descriptor.__get__(descriptor, a, A)
这里包含的重要概念是 descriptor.
在上面的属性访问算法中, 涉及到了对象 a 的 __dict__ 属性. 创建类 A 时会设置一个 tp_dictoffset 域,
根据名字看是指 instance 对象中 __dict__ 偏移位置. 图12-24 描述此结构. 在 instance 中有一个字段
放置 dict 对象, 偏移由 type->tp_dictoffset 给出. 找到该 dict 之后, 从该 dict 中查找出名为 name 的
属性, 作为值返回. dict 本身在对称的 setter 中第一次发现为 null 时被创建.
回顾前面的 PyType_Ready() 中, 填充的 tp_dict 即是名字->descriptor.
几个有重大影响的操作 __get__, __set__, __delete__.
descriptor 有数种: PyWrapperDescrObject, PyMethodDescrObject, data-descriptor
在 Python 虚拟机访问 instance 对象的属性时, descriptor 影响对属性的选择. 首先会在 instance.__dict__
中寻找属性, 也会在 instance.__class__.mro 中寻找属性. 前者称为 instance 属性, 后者称为 class 属性.
书上根据效果总结两条规则(...看着有点乱...):
1. 按照 instance 属性, class 属性的顺序选择属性, 即前者优先于后者.
2. 如果在 class 属性中发现同名的 data descriptor, 则该 descriptor 优先于 instance 属性.
(这里需要看介绍 python 细节的书, 以及看源码)
当最终获得的属性是一个 descriptor 时, 不是返回 descriptor 本身, 而是调用其 descriptor.__get__
后将结果返回.
(我觉得这里应先看看 python 语言的书, 并关注下属性访问细节, 然后再看源码会更好)
另有细节区别于 class.tp_dict 中的 descriptor 和 instance.tp_dict 的. 需要跟踪源码了解.
这里 descriptor 对属性访问的影响包含两方面: 1. 访问顺序; 2.访问结果.
后者是类的成员函数调用的关键.
当访问 a.f() 的时候, 先获取 a.f 的属性并压栈, 并使用 CALL_FUNCTION 指令调用, 那么 self 参数在哪里?
引用爱因斯坦: 为了相对论, 必须抛弃绝对时空的观念.
这里 a.f 返回的不是 PyFunctionObject, 由于 A.f 是一个 descriptor, 从而调用 A.f.__get__:
// funcobject.c // 绑定函数到一个对象. PyObject *func_descr_get(PyObject *func, PyObject *obj, PyObject *type) { // ... 其它检查略 ... return PyMethod_New(func, obj, type); } // classobject.c PyObject *PyMethod_New(PyObject *func, PyObject *self, PyObject *class) { ... 检查等代码略 ... var m = new PyMethodObject(); m->im_func = func; m->im_self = self; // 这里重点是 method 中绑定了 self 对象. m->im_class = class; // 还绑定了所属类? return m; }
(我觉得概念上可理解 method 即是 js 中的 f.bind(this))
这样 a.f 首先得到的是一个 PyMethodObject. 在 python 中, 将 PyFunctionObject 对象和一个 instance
对象通过 PyMethodObject 结合在一起的过程: 称为成员函数的绑定.
这样, 我们就知道了对 a.f() 的调用实现细节步骤:
1. 找到 a.f 属性, 是一个 function-obect;
2. 绑定 function-objet + a作为self 形成一个 method-object
3. 调用 method-object, 等价于调用 function-object(a as self).
第3步在 call_function() 函数中有体现:
// ceval.c PyObject *call_function(...) { ... 前面略 ... else if (method is PyMethodObject) { PyObject *self = method->get_self(); // 取出绑定的 self 参数. PyObject *func = method->get_function(); // 取出实际函数对象. // 调用该 func, 带上 self 参数(语义上) ... do_call(func, self, 其它参数...) } else ... }
书上总结 Python 运行模型, 高度简化为两条规则:
1. 在某个名字空间寻找符号对应的对象.
2. 对该对象进行某些操作.
Bound Method 和 Unbound Method
Python 中引用类的函数有两种形式, 一是 Bound method 如用 a.f 形式引用的; 二是 Unbound Method,
如用 A.f 形式引用的. 内部区别即是 PyMethodObject 和 PyFunctionObject 的区别.
由于每次对成员函数的调用, 都会创建新的 method-object 对象, 这样开销比较大. 实验: 见书.
前述调用 instance 成员函数时, 最关键的动作即是从 func->method 的转变, 此转变由 descriptor 完成.
在 python 内部, 存在着各种各样的 descriptor. 下面简介如何用 descriptor 实现 static method.
通过 python 内置的 staticmethod 函数(实际是一个 type object), 将一个函数转变为属性...
(细节暂时略, 需要先看 python 书了解下 staticmethod 的语义和应用)
(本章结束)
由于类机制是十分复杂, 且极端重要的部分, 为了(差不多)学懂, 也得多花点时间看书,实验,翻看源码, 不能着急.