您的位置:首页 > 编程语言 > Python开发

看书 Python 源码分析笔记 (七) 类机制

2015-12-23 00:00 726 查看

第12章 Python 虚拟机中的类机制

从 Python 2.2 开始有了两套类机制: classic class, & new style class. 随着演进前者渐渐消失, 仅考察后者.

Python 中的对象模型

于 python 2.2 之前, 内置 type 不能被程序员写的类继承. 此后使用统一的类型机制, 使得两者概念一致.

面向对象理论中的两个核心概念: 类和对象. 另有 类对象, 对象类等容易混乱的概念...
在 python 中, 使用一种统一的表达形式:
1. 对于 class 对象 A, 用 <class A> 表示名为 A 的 class 对象.
2. 对于 instance 对象, 用 <instance a> 表示名为 a 的 instance 对象.

用术语 type 来表示类型(概念), 不是类型对象.

python 三种对象之间存在两种关系:

1. is-kind-of 关系: 对应基类与子类之间的关系.
2. is-instance-of 关系: 对应类与实例之间的关系.

Python 提供一些方法和属性来检查关系.
1. 对象的 __class__ 属性, 或 type(obj) 方法: 得到对象所属类(is-instance-of).
2. __bases__ 属性获得类的基类.
3. issubclass() 内置方法判定类-类继承关系.
4. isinstance() 内置方法判定实例-类关系.

(参见书上的描述和图).

<type 'type'> 是一个 metaclass. 创建一个 class 对象的关键之处在于 metaclass.

class 对象有一种类似于 "波粒二相性" 的特殊性质, 它们即是 metaclass 的 instance, 也是别的 instance 的 class.

Python 中, 不仅只有函数可以被调用, 一切对象都有可能被调用.
概念: 可调用性 (callable). 只要一个对象的 class 实现了 __call__ 操作, 则这个对象就是一个可调用的对象.

熟悉 C++ 的可看做是对操作符 () 的重载.

Python 启动时, 会对类型系统(对象模型) 进行初始化, 会在内置类型对应的 PyTypeObject 中填充一些东西,
从而完成内置类型从 type 对象到 class 对象的转变.

参见 object.c 中 _Py_ReadyTypes() 函数, 其会调用各内置类型的 typeobject.c PyType_Ready() 函数.

// 位于 typeobject.c; 这里用伪代码表示语义即可.
int PyType_Ready(PyTypeObject *type) {
PyTypeObject*& base_type = type->tp_base;
// 1. 如果未指定 type 的基类, 则设置 type 的基类为 PyBaseObject_Type.
if (base_type == NULL)
base_type = PyBaseObject_Type;

// 2. 如果此类的基类未就绪(ready), 则先(递归)准备基类.
if (not base_type.ready?)
PyType_Ready(base_type);  // 递归调用.

// 3. 如果未设置 type, 则继承自基类
if (type->ob_type == NULL)
type->ob_type = base_type->ob_type;  // 这里需要细究 ob_type 含义

...
}


这里伪代码 (1) 处, 会尝试获取/设置此 type 的基类, 由 type->tp_base 指定. 如果未指定则使用内部的
base-object 作为基类, 当然 base-object 自身的基类为 null, 再没有 base-base-object 了.

伪代码 (3) 处设置 ob_type 信息, 也即对象(类对象?)的 __class__ 返回的信息. (这里我略糊涂, 需要仔细看...)
更进一步地说, 这里设置的 ob_type 就是 metaclass. 缺省用基类的, 而 base-object 的 ob_type 是
metaclass, 即 PyType_Type ...

实验: 类对象的 __class__, 与实例对象的 __class__ 还是有区别的 (书上语言不够精确...):

a = object()  # 新建一个内建类型 object 的 instance a
a.__class__   # (实例对象的 __class__) 输出为 <class 'object'>
object.__class__  # (类对象的 __class__) 输出为 <class 'type'>

下面继续 PyType_Ready():

int PyType_Ready(PyTypeObject *type) {
// ...

// 4. 处理 bases: 基类列表
Tuple bases = type->tp_bases;
if (bases == NULL) {
// 如果未填写(初始)基类列表, 则根据 base 情况填充 bases
type->tp_bases = new Tuple(base);
}
...
}

这里检查 base 为 NULL 的特殊情况处理略去(如对于 base-object 类型, 其没有 base).

Python 支持多重继承, 这是 python 相比其它语言(含脚本的, 非脚本的)的一个特点所在. 某种程度上, 也是我想
深入了解其如何实现的重要原因. (否则没有特点的东西, 为什么要学~~?)

下面继续填充重要的 tp_dict 于 PyType_Ready():

int PyType_Ready(PyTypeObject *type) {
// ... 见前面 ...

// 5. 设定 tp_dict
if (type->tp_dict == NULL) {
type->tp_dict = new PyDictObject();
}

// 6. 将与 type 相关的 descriptor 加入到 tp_dict 中.
add_operators(type);
if (type->tp_methods != NULL)
add_methods(type, type->tp_methods);
类似的 add_members(...), add_getset(...)
...
}

此阶段, 完成将 ("__add__", &nb_add) 对加入到 tp_dict 的过程. 其它几个 add_xxx() 也完成类似填充任务.
名字->函数对关系存放在 slotdefs 全局数组中.

下面介绍 python 内部概念: slot.

在 python 内部, slot 可被视为表示 PyTypeObject 中定义的操作 (类似 C++/Java 的一个虚函数),
一个操作对应一个 slot. 使用 slotdef 结构表示所需信息:

// descrobject.h
struct slotdef {  // 原名 wrapperbase, 别名为 slotdef
char *name;     // slot 名字, 如 '__add__'
int offset;     // ?函数在 xxx 中的偏移量
void *function; // 实现此操作的函数: slot function.
wrapperfunc wrapper;
char *doc;      // 此函数的说明.
int flags;
PyObject *name_strobj;
}


与 PyTypeObject 类似的 PyHeapTypeObject:

// object.h
struct PyHeapTypeObject {
PyTypeObject ht_type;  // 可认为从 PyTypeObject 继承.
PyNumberMethods as_number;    // 提供数学操作函数
PyMappingMethods as_mapping;  // 提供集合操作
PySequenceMethods as_sequece; // 提供序列操作
PyBubfferProcs as_buffer;     // 更多...
PyObject *ht_name, *ht_slots;
};


下面再看 Python 预定义的 slotdefs[] 集合:

// typeobject.c
slotdef slotdefs[] = {
// BINary 二元运算符的槽 __add__, nb 我想是 number 的缩写.
BINSLOT("__add__", nb_add, slot_nb_add, "+"),
更多 bin 如 __sub__, __mul__ ...
更有 UNSLOT, NBSLOT, RBINSLOT... 等多种数学的...

// SQ Sequence 序列操作的槽
SQSLOT("__len__", sq_length, slot_sq_length, ...),
SQSLOT("__getitem__", sq_item, ...),
更多 seq slot ...

// MP Mapping 集合操作的槽
MPSLOT("__getitem__", mp_subscript, slot_mp_subscript, ...),
更多 mp slot ...

// TP type 相关操作
TPSLOT("__hash__", ...),
TPSLOT("__call__", ...),

... 大量的槽定义 ...
}


有的槽对应函数名字和别的槽函数名字一样, 如 seq 和 map 都提供有 __getitem__ 槽, 则在 tp_dict 中选择
sq_item() 函数还是 mp_subscript() 呢?

为解决此问题, 使用 slot 中的 offset 值来排序. 注意 PyHeapTypeObject 中各 methods 组的定义顺序,
此顺序隐含着优先级. 如 map 在 seq 前面, 则有 offset(func, map) < offset(func, seq) 关系成立.
这样, 如果一个 type-object 即定义了 map.__getitem__, 又定义有 seq.__getitem__, 则 python 选择
map 的 __getitem__ 函数. 例如 PyList_Type 即是这样一个例子.

对 slotdefs 的排序在 init_slotdefs() 函数中完成. (伪代码略).

概念: descriptor.

// descrobject.h
struct PyWrapperDescrObject {
PyObject 公共 HEAD 部分;
PyTypeObject *d_type;
PyObject *d_name;
struct slotdef *d_base;  // 指向 slotdef 信息.
void *d_wrapped;  // this can be any function pointer.
}


在 Python 内部, 存在多种 descriptor, 与 PyTypeObject 中的操作对应的是 PyWrapperDescrObject.
这里我们用 descriptor 特指此 wrapper.

一个 descriptor 包含一个 slot, 字段 d_wrapped 就是包装的实现函数. 如对应 __getitem__ 的此
descriptor.d_wrapped 即为 &mp_subscript (map 的).

(我们可以将 descriptor 看做是 C/C++ 函数指针在 python 中的一个包装, 用以方便在 python 中引用...)

由于 PyTypeObject 与 PyHeapTypeObject 结构有差异, slotptr() 函数要做必要的位置转换, 才能对应
到函数指针的槽位置. slotptr() 函数伪代码...略...

Python 对 PyTypeObject 进行了改造, 向 tp_dict 添加了 name->descriptor 的映射项.
书上有一个很好的 图12-8 表述了 type,dict,descriptor(wrapper) 之间的关系... (仔细看)

类似于 add_operator() 的其它几个函数分别添加 PyMethodDescrObject, PyMemberDescrObject,
PyGetSetDescrObject. (可以看做是 C 对应方法/数据在 python 中的 wrapper).

如果用户定义的类, 添加了自定义的 python 的 special method, 如 __repr__, 则 python 是如何调用到
该自定义方法的?

查看 slotdefs[] 中含有的 "__repr__" 项, 当 Python 创建类 A 时, 会检查用户是否重写了 __repr__ 操作.
如果有重写, 则找到 __repr__ 对应的 slot, 将函数指针替换为 slot 中指定的 &slot_tp_repr. 这样后面执行
A.tp_repr 时, 实际执行 slot_tp_repr() 函数:

PyObject *slot_tp_repr(PyObject *self) {
var func = lookup_method(self, "__repr__" ...);
// 检查 func 为 NULL 及错误报告略去.
result = PyEval_CallObject(func, NULL);
return result;
}

因而这里会查找 __repr__ 属性对应的值(是一个函数对象), 正好是在 A 中重写的函数, 这样就完成了对默认 repr
行为的替换. 图 12-10 显示此时类 A 的结构, 和图 12-8 对比看更好.

这里应感谢作者画的图, 我就懒得画图..., 最多纸上画画...

确定 MRO

这里 MRO 指 Method Resolve Order. 指 class 对象的属性/方法解析顺序.
由于 Python 支持多重继承, 此时必须设置按照何种顺序解析属性. 考察例子:

# mro.py -- 考察解析顺序
class A:
def show(self):
print "A::show"

class B:
def show(self):
print "B:show"

class C(A):     # C 继承自 A, 即也继承 show() 方法
pass

class D(C, B):  # D 有两个基类 C,B, 间接继承 A::show, 直接继承 B::show
pass

d = D()         # 实例化一个 D 对象
d.show()        # 哪个 show 方法会被调用?


这里对于多继承就要回答这个关键问题了. Python 内部在 PyType_Ready 中通过 mro_internal 函数完成
对一个类型的 mro 顺序的建立. Python 将创建一个 tuple, 放置一组 class 对象, 这些 class 的顺序即是
虚拟机在解析属性时的 mro 顺序. 最终这个 tuple 保存在 PyTypeObject.tp_mro 中.

书上说 mro_internal 内部实现繁杂, 未再深入. 仅概念上剖析确定 mro 顺序.
(如果想深入研究 python 多继承, 以后还是要深入的...)

如上面例子, 类 D 的 mro 为 (D, C, A, B, object). 见图 12-12.

实验: 打印 D.__mro__, 在我编译出的 python 2.5 中没有此属性?
换 python 3.x, 打印出来的确是 (D, C, A, B, object).

这样 mro 列表中实际存储 class 对象的所有直接和间接基类. Python 会将 class 对象自身没有设置而基类中
设置了的操作(函数)复制到 class 对象中, 从而实现多继承语义.

// typeobject.c
int PyType_Ready(*type) {
...
bases = type->tp_mro;  // 这里的关键点是按照 mro 顺序继承基类的操作.
for (i = 1; i < length(bases); ++i) {
inherit_slots(type, (PyTypeObject *)bases[i]);
}
...
}

函数 inherit_slots() 中会复制相当多的操作:

void inherit_slots(PyTypeObject *type, PyTypeObject *base) {
if (base 有这些数学操作) {
COPYNUM(nb_add);  // number add '+'
COPYNUM(nb_subtract);
... 几十个 ...
}
if (base 有 seq 操作)
复制 seq 操作函数到槽中...
同样对 map, buff, 以及 print, repr, str 等函数槽.
}

我觉得可以把这个过程看做是填写 class 的虚表 (virtual-table).

此后 PyType_Ready 还完成最后一个重要的动作: 设置基类中的子类列表. 在字段 tp_subclasses 中:

int PyType_Ready(PyTypeObject *type) {
...

// 填充基类的子类列表
bases = type->tp_bases;
for (int i = 0; i < length(bases); ++i)
bases[i].add_subclass(type)
...
}

验证一个类的子类: object.__subclasses__() --> 得到一大堆子类的列表.

小结 PyType_Ready() 函数的工作:
1. 设置 type 信息, 基类即基类列表.
2. 填充 tp_dict.
3. 决定 mro 顺序.
4. 基于 mro 从基类继承操作.
5. 设置基类的子类列表.

(由于设置了基类的子类, 这样父类和子类之间存在了双向引用关系)

============

本章比较长也十分重要, 因此这里断开一篇, 复习一下下篇接着学习...
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: