Python 源码的考古(三) 读 0.9.1 源码2
2015-12-29 00:00
776 查看
继续读 Python 0.9.1 的源码.
以 list, dict 两个很常用的内建类作为例子. 查看一下源码:
现在已知通过提供 getattr 函数, 则通过语法 obj.xxx 即可(试图)访问到 obj 的属性 xxx (也可以是函数).
所以看看 list_getattr(), dict_getattr() 即可了解, 两者实质相似:
通过 findmethod() 返回的是一个 methodobject 对象, 类似于查找 class/instance 的函数返回的那个
classmethodobject 包装.
当执行虚拟机的 CALL 指令时, 如果待执行的对象类型是 methodobject 则:
这里表明大致有四种可以执行的东西: 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.
这里 codeobject (应该是)由解析/编译部分负责生成, 所以这里先不讨论.
函数 newfuncobject() 创建函数对象:
于是, 在 python 中定义的函数就被包装为 funcobject (存入某个名字空间中). 在调用时 CALL 指令
判断是 funcobject 类型, 则进入调用该类型函数的通道.
再验证一下 builtin 等内部模块, 它们的函数是如何在 python 中能被调用的, 估计也是包装为 method 或 func
object ? (合理推测)
这样, 当在 python 中输入 dir() 命令时, 会查找名字空间(LGB规则等, 反正找到上面创建的 method_object),
从而在 CALL 指令处得以判断出要执行的对象类型是 method_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 参数.
list 函数定义. 在 getattr() 中使用 findmethod() 找到指定名字的函数.
另一种需要是定义一个类的可访问属性列表, 通过类似的定义 memberlist 实现, 例子如下:
如果既有函数(需要 methodlist[]), 又有属性(需要 memberlist[]), 那就需要两次查找在两个数组中.
似乎我还没看到有这种对象, 其它对象要么只有函数, 要么只有属性, 有点怪...?
估计这里的 methodlist, memberlist 未来发展为各种 descriptor. 这样, 灵活的 descriptor 也不是/会无缘无故
就出生了, 对此我们不要过于惊异, 因为历史即会遗留问题, 也会解决问题.
我有一个疑问, 为什么栈帧对象不能建立在 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 源码解析》一书遗憾地未述及的地方.
内建类的更多了解
前面了解了程序员定义的 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 源码解析》一书遗憾地未述及的地方.
相关文章推荐
- 我的Python 学习之旅 从0开始的小白
- 最新最全的Python 学习路线图
- PYTHONPATH
- python zip map filter lambda的简单应用
- selenium使用chrome时,报错ignore certificate errors
- 备份脚本-学习《简明python教程》
- Python笔记-几种取整方式
- Python笔记-均值列表
- 动态改变python的搜索路径
- Python_使用ElementTree解析xml文件
- pandas在非IPython模式下的绘图显示
- 从指定的搜索路径寻找文件
- Python模块常用的几种安装方式
- 一小时学会用Python Socket 开发可并发的FTP服务器!!
- [Python标准库]copy——复制对象
- python+正则表达式获取ed2k url
- Python Paramiko模块与MySQL数据库操作
- python学习之字符串(下)
- python学习之1 numpy常用的函数
- Python获取web页面信息