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

Python 源码的考古(三) 读 0.9.1 源码2

2015-12-29 00:00 776 查看
继续读 Python 0.9.1 的源码.

内建类的更多了解

前面了解了程序员定义的 old-style-class 的成员访问机制, 则内建类的成员是如何访问的?
以 list, dict 两个很常用的内建类作为例子. 查看一下源码:

// listobject.c
typeobject Listtype = {  // list 类型的信息表.
...无关的忽略...
list_getattr,       // 支持访问 list 中属性或函数.
&list_as_sequence,  // list 当然是序列, 所以提供序列访问函数集.
}

// dictobject.c
typeobject Dicttype = {
dict_getattr,       // 类似 list 的.
&dict_as_mapping,   // 支持 map 访问函数.
}


现在已知通过提供 getattr 函数, 则通过语法 obj.xxx 即可(试图)访问到 obj 的属性 xxx (也可以是函数).
所以看看 list_getattr(), dict_getattr() 即可了解, 两者实质相似:

// list 对象获取属性的实现函数.
object *list_getattr(listobject *obj, char *name) {
return findmethod(list_methods, obj, name);
}

// 这里 list_methods[] 是一个预定义的数组.
struct methodlist list_methods[] = {
{ 'append', list_append },   // 访问属性 'append' 映射到 C 函数 list_append.
...略...
}

// 而函数 findmethod() 即遍历一下这个数组, 查找 name 对应的那个 C 函数.
// 然后包装一下返回.
object *findmethod(methodlist *ml, object *op, char *name) {
for-each (m in ml)
search ml->name == name   // 查找.
// 找到则包装为一个 methodobject, 内含 C 函数指针, 对象等.
return new method_object(name, ml->cfunc, op);
...
}


通过 findmethod() 返回的是一个 methodobject 对象, 类似于查找 class/instance 的函数返回的那个
classmethodobject 包装.

// 一般用于包装一个 C 函数, 以能够从 python 中调用.
struct methodobject : public object {
char *m_name;   // 函数名字.
method m_meth;  // C 函数指针, 类型为 object *(*method)(object*,object*).
object *m_self; // 可选的绑定的 self 对象.
};


当执行虚拟机的 CALL 指令时, 如果待执行的对象类型是 methodobject 则:

// 执行 CALL 指令的伪代码:
case UNARY_CALL:
v = POP();   // 要执行的函数对象.
if (is_calssmethodobject(v))
1. 当做 class-method-object 来调用 (定义在类中的成员函数, python->python).
else if (is_funcobject(v))
2. 当做 func-object 来调用 (普通的用 def 定义的全局函数, *->python).
else if (is_methodobject(v))
3. 当做 method-object 调用 (即 python->C)
else if (is_classobject(v))
4. 当做用户定义的类, 创建新实例 (python->python)
else 错误.

这里表明大致有四种可以执行的东西: class-method, func, method, class.
1. 从 C->C (这里用 -> 表示调用)没有问题.
2. 从 *->python 上面给出了三种形式, 应该穷举了所有 *->python 调用类型?
3. 从 python->C 通过 method-object 包装了一下, 我估计全局定义的 builtin C 函数大致也是这样.

这些类型我们验证一下, 其中 class-method-object 前篇在看新建 instance 对象时已知了.

下面 func-object 我们用定义一个全局函数的方式来验证 def f(): pass.

// 在 ceval.c 中虚拟机指令 BUILD_FUNCTION:
case BUILD_FUNCTION:
v = POP();   // 函数体前面创建为 codeobject, 并压入堆栈.
x = newfuncobject(v, ...);   // 这里创建 funcobject.
...


这里 codeobject (应该是)由解析/编译部分负责生成, 所以这里先不讨论.
函数 newfuncobject() 创建函数对象:

struct funcobject : public object {
object *func_code;     // 指向 codeobject
object *func_globals;  // 名字空间.
}

object *newfuncobject(...) {
(创建实例用) new funcobject, 然后填写属性, 然后返回...
}

于是, 在 python 中定义的函数就被包装为 funcobject (存入某个名字空间中). 在调用时 CALL 指令
判断是 funcobject 类型, 则进入调用该类型函数的通道.

再验证一下 builtin 等内部模块, 它们的函数是如何在 python 中能被调用的, 估计也是包装为 method 或 func
object ? (合理推测)

// bltinmodule.c -- bulitin-module, 在初始化时被调用.
void init_builtin() {
initmodule('builtin', builtin_methods);
...
}

// 这里 builtin_methods[] 数组给出了 name->Cfunc 名字到C实现函数的映射表, 如:
xxx builtin_methods[] = {
'abs' -> builtin_abs,  // 伪代码!
'dir' -> builtin_dir,
...
}

// 然后在 initmodule() 中将这些方法注册到 dict 中.
object *initmodule(name, methods[]) {
...
for-each ml in methods[]
v = new method_object(ml->name, ml->cfunc);  // 重点: 包装为 method_object.
module->dict[ml->name] = v;  // 插入到 dict 中.
}

这样, 当在 python 中输入 dir() 命令时, 会查找名字空间(LGB规则等, 反正找到上面创建的 method_object),
从而在 CALL 指令处得以判断出要执行的对象类型是 method_object, 即可被调用的那几种东西.

参数解拆

还有一个小问题, 一般函数调用的时候是有参数的, 当从 python 调用到 C, 参数是如何从 python object
转换为 C 所需要的类型的? (转换参数的这种麻烦事我称为参数解拆...)

设我们调用内建函数 max(1,2,3) 通过给出多个参数(如果给出更多参数其本质不变). 则在 python 的早期
版本是这么做的:
1. 首先多个参数分别被计算出来, 并压入堆栈中. 数量在编译时是已知的.
2. 将这些参数 POP 出来并打包为一个 tuple, 然后将此 tuple 压入堆栈. (可称为参数打包)
3. 在指令 BINARY_CALL 处取出 func, tuple(做为 arg), 调用 func 以此 arg.
这里编译时给出 BINARY_CALL 指令, 而非 UNARY_CALL, 后者表示不带参数的调用.

所有 C 函数将被从 python 中调用的, 其原型都是 object *(*cfunc)(object *self, object *args),
即必须返回 object*, 第一个参数是 self(或 instance, 可能为 null), 第二个参数是一个 object*,
当有多个参数时, 第二个参数即是一个 tuple.

查看 builtin_max(), builtin_range() 等可以带多个参数的函数中, 函数需要自己负责解拆参数, 必须自己
检查参数的类型, 如果是 tuple 则可能还要从 tuple 中分解出一个一个的参数. 这样做每个函数要辛苦一点.
在 modsupport.c 中有一组(很多!) get-xxx-arg() 函数, 用于各种参数组合的 argument list handling,
我们知道组合会产生组合爆炸, 如果每遇到一种组合就写一个函数, 那以后只这一件事就整死开发者了,
所以后面版本这里发展出了 get_args()/parse_args() 的复杂的/灵活的/难理解的解决方法...

再实验了一下, 对于调用不带参数的 max() 生成指令为 UNARY_CALL 无参调用; 调用 max(1个参数)
用的是 BINARY_CALL, 但没有 tuple 打包参数过程, 估计是因为只有一个参数; 调用 max(1,2,3...多参数)
时用 BINARY_CALL, 但编译给出了 tuple 打包参数指令. 估计这是早期 python 参数传递的唯一方案,
因为当时还没有设计 keywords 参数和 *list 参数.

访问属性的另一种实现

前一篇中, 代码中看到使用 methodlist[] 来定义一个类的可用函数列表, 如出现在 listobject.c 中的
list 函数定义. 在 getattr() 中使用 findmethod() 找到指定名字的函数.

另一种需要是定义一个类的可访问属性列表, 通过类似的定义 memberlist 实现, 例子如下:

// 在 compile.c 中为 codeobject 定义可访问的属性:
struct memberlist code_memberlist[] = {
// 属性名字    值类型      在结构中的偏移位置.
{ 'co_code', T_OBJECT, OFF(co_code) }
...
}

// 当访问此对象的属性时, 会查找上表.
object *code_getattr(object *co, char *name) {
for-each (ml in code_memberlist[])
if (ml->name == name)  // 字符串比较相等.
return co->OFFSET 指定偏移处的值.
}


如果既有函数(需要 methodlist[]), 又有属性(需要 memberlist[]), 那就需要两次查找在两个数组中.
似乎我还没看到有这种对象, 其它对象要么只有函数, 要么只有属性, 有点怪...?

估计这里的 methodlist, memberlist 未来发展为各种 descriptor. 这样, 灵活的 descriptor 也不是/会无缘无故
就出生了, 对此我们不要过于惊异, 因为历史即会遗留问题, 也会解决问题.

其它对象

在虚拟机运行时, 会建立 frameobject 表示/模拟栈帧, 栈帧通过指针链接为一个单链表.
我有一个疑问, 为什么栈帧对象不能建立在 eval() 函数的 C 栈上? 也许必须满足对象在 heap 上的条件?

traceback 对象提供异常栈帧跟踪, 提供几个简单属性可让 python 访问, 类似于 frameobject.

对象 codeobject 由 compile 部分负责生成, 以后研究 parse/compile 时再细究.

regexpobject 正则对象由 regexp 内建模块提供. 正则比较独立/复杂, 需要再看.

存在一个 xxobject.c 文件, 作为实现一种新对象的模板文件. 要实现一种新对象, 通常是
1. 定义一个 xxxobject 结构, 放入自己需要的字段.
2. 定义 Xxxtype 数据, 填入所需信息/虚函数.
3. 实现 new-xxx-object() 函数, 这样能实例化此对象.
4. 实现 xxx-dealloc(), xxx-repr(), xxx-getattr() 等函数, 可选.
5. 如果有 getattr(),setattr(), 选用 methodlist[] 或 memberlist[] 机制实现它.
6. 或者还需要在 initxxx() 中加入点初始化代码, 使得 builtin 或 globals 或 modules 中能找到对象.

moduleobject 提供模块支持, 主要就是名字和一个字典.

fileobject 提供文件读写支持, 注解提到这个对象应改造为 io 模块.

系统的一些异常(存放在 builtin 中的预定义字符串) 在 bltinmodule.c 中初始化.

除了词法/语法/编译部分外, 对象差不多就这些了 (偶有遗漏也问题不大).

内存分配在 0.9.1 里面没有, GC(垃圾回收)没有, 进程/线程也没有, 所以我们的 0.9.1 的考古之旅这里
就暂时结束了. 下一步我想大概看看后续版本如正式的 1.0 版,1.5版, 然后主要研究一下词法/语法/编译部分.
这一部分是《Python 源码解析》一书遗憾地未述及的地方.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: