以下是本人阅读此书时理解的一些笔记,包含一些影响文义的笔误修正,当然不一定正确,贴出来一起讨论。
注:此书剖析的源码是2.5版本,在python.org 可以找到源码。纸质书阅读,pdf 贴图。
文章篇幅太长,故切分成3部分,这是第一部分。
p9:int_repr 函数中 PyObject_Print(str, stdout, 0); stdout 修改为 out
p23 & p263:tp_as_number.nb_add 修改为 tp_as_number->nb_add
p23 & p271:tp_as_mapping.mp_subscript 修改为 tp_as_mapping->mp_subscript
tp_as_sequence.sq_item 修改为 tp_as_sequence->sq_item
p25:运行时整数对象及其类型之间的关系理解
对于int(10) 的 ob_refcnt 来说可以理解为多个ref 引用了这个对象,ob_type 是指向其类型对象的指针,ob_ival 是具体数值。
int(10) 是PyIntObject 的实例对象,比PyObject 多一个ob_ival 成员,PyVarObject 比PyObject 多一个int ob_size; 即表示元素个数,当
然具体的 如 PyStringObject 还会有其他的成员,如下所示。可以认为 PyObject 是 整数类型等定长对象的头, PyVarObject 是 str 等不
定长对象的头,如下所示,注意下面带EXTRA 字样的宏 只有在debug 模式下才存在,故可以忽略不计。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD \ _PyObject_HEAD_EXTRA \ Py_ssize_t ob_refcnt; \ struct _typeobject *ob_type; /* PyObject_VAR_HEAD defines the initial segment of all variable-size * container objects. These end with a declaration of an array with 1 * element, but enough space is malloc'ed so that the array actually * has room for ob_size elements. Note that ob_size is an element count, * not necessarily a byte count. */ #define PyObject_VAR_HEAD \ PyObject_HEAD \ Py_ssize_t ob_size; /* Number of items in variable part */ /* Nothing is actually declared to be a PyObject, but every pointer to * a Python object can be cast to a PyObject*. This is inheritance built * by hand. Similarly every pointer to a variable-size Python object can, * in addition, be cast to PyVarObject*. */ typedef struct _object { PyObject_HEAD } PyObject; typedef struct { PyObject_VAR_HEAD } PyVarObject; |
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
typedef
struct {
PyObject_VAR_HEAD long ob_shash; int ob_sstate; char ob_sval[ 1]; /* Invariants: * ob_sval contains space for 'ob_size+1' elements. * ob_sval[ob_size] == 0. * ob_shash is the hash of the string or -1 if not computed yet. * ob_sstate != 0 if the string object is in stringobject.c's * 'interned' dictionary; in this case the two references * from 'interned' to this object are *not counted* in ob_refcnt. */ } PyStringObject; |
PyInt_Type、PyBaseObject_Type、PyType_Type 都是PyTypeObject 的实例对象。PyInt_Type 的 tp_base 指向其基类对
象 PyBaseObject_Type,而他们的 ob_type 都指向 PyType_Type,假设定义 class A(object):pass 那么 A.__class__ 、
int.__class__、 type.__class__ 、object.__class__ 都是
Python 2.2 之前的内置类型不能被继承的原因就在于没有在 type 中寻找某个属性的机制。
需要注意的是类型对象是超越引用计数规则的,每一个对象指向类型对象的指针不被视为对类型对象的引用,即类型对象永远不会
被析构。当然一个对象被析构也不一定马上释放内存,往往都是大量采用内存对象池技术(要么预先分配,要么将销毁的对象添加进池),避免频繁地
申请和释放内存。比如位于[-5,257)之间 的小整数缓存在小整型对象池中,a=256, b=256, a is b 是 True 的,也就是引用着同个对象。短字符串同理,注意字符串性能相关的 '+' 操作和 join 操作:每次 '+' 操作都需要新创建对象,性能较差;join 先计算结果对象的总长度,创建一个结果字符串对象,然后拷贝数据到结果内存位置,所以性能较好。
如下的PyInt_Type 的初始化代码,可知初始化 ob_refcnt 为1,而 ob_type 为 PyType_Type 的地址;PyType_Type 的初始化也是类似
的,故其 ob_type 指向自身,但它的 tp_name 为 ”type" 。 tp_basicsize 和 tp_itemsize 分别表示对象基本大小和元素大小,对于
PyIntObject 来说没有元素大小,如果是str 即 PyVarObject,tp_itemsize 即 sizeof(char),加上一个 ob_size 参数,也能在创建 instance 对象时确定
分配的内存大小,即 /* For allocation */
1
2 3 4 5 6 7 8 9 10 11 12 |
#define PyObject_HEAD_INIT(type) \
_PyObject_EXTRA_INIT \ 1, type, PyTypeObject PyInt_Type = { PyObject_HEAD_INIT(&PyType_Type) 0, /* ob_size */ "int", /* tp_name */ sizeof(PyIntObject), /* tp_basicsize */ 0, /* tp_itemsize */ ..., }; |
Python 对象的多态性正是利用函数传递PyObject* 而不是具体的PyIntObject* 等来实现,具体判断是什么类型对象是通过动态判断对
象的 ob_type 来实现,考虑如下的代码:
1
2 3 4 |
void Print(PyObject* object)
{ object->ob_type->tp_print(object); } |
如果传递给 Print 函数的是 PyIntObject* ,那调用的是 PyIntObject 对象对应的输出操作,如果是PyStringObject* 同理。
p87: 倒数第二段尾句,如果被设置,则不会返回Dummy 态 entry。Dummy 修改为 Unused
p98: 代码中的一个for 循环可以不需要 PyObject* value = entry->me_value; 第二个for 循环可以不需要 PyObject* key = entry->me_key;
第二个 for 循环打印应该是 (value->ob_type)->tp_print(value, stdout, 0);
p101: 模拟实现的Small Python 并没有贴出完整代码,顺着作者思路写完了,代码在 https://github.com/JnuSimba/Small_Python
p115: 在Python 中类、函数、module 都对应着一个独立的名字空间PyDictObject,因此都会有一个PyCodeObject 对象(code block)与其对应(对
应一个PyFrameObject),可以通过 __code__ 访问到,这些PyCodeObject 对象以嵌套递归(里面的PyCodeObject 对象存储在外面对象的
co_consts 里)的方式写入pyc 文件,包括co_code 指向的PyStringObject 对象即字节码指令,最终写入文件的是string or number。实际上整个字节
码指令序列就是一个在C中普通的字符数组,只不过每个指令(100来个,opcode.h 中宏定义为一个具体数值)有预定义的含义,在 interpreter main
loop 中不断取出每条指令,进而 switch case 进行指令实现(即Python 解释器 C 源码实现)。如下使用 dis.dis 展示的字节码指令:
1 0 LOAD_CONST 0 (1)
3 STORE_NAME 0 (i)
最左面第一列表示字节码指令对应的源码在py 文件的行数,左起第二列是当前字节码指令在co_code 的偏移位置,第三列显示当前字节码指令,最后
一列是指令参数(括号内是类似指令提示的东东)。比如 LOAD_CONST 0 所做的操作就是从 f->f_code->co_consts 常量表(PyTupleObject)中取出
序号为0的元素即整数对象1,将其压入虚拟机的运行时栈中;STORE_NAME 0 先从符号表 f->f_code->co_names(PyTupleObject)获取序号为0
的元素的作为变量名,将前面获取到的整数对象从栈中pop 出作为变量值,将(i, 1)添加到 f->f_locals 中。以上可以认为是字节码执行的缩影,即不
断在运行时栈和名字空间内进行运算。
注:一个字节码指令1个字节,一个指令参数2个字节,第二列的偏移值就是这么计算得来的。
p137: 这个栈空间的大小存储在 f_stacksize 中 ... f_stacksize 修改为 co_stacksize 即是PyCodeObject 的成员。
Python 执行的某个时刻的运行时环境如下图所示:
如p115 条目所说,PyFrameObject 的f_locals、f_globals、f_builtins 分别指向不同的名字空间,对于类or 函数的 f_locals 和 f_globals
指向往往是不一样的(实际上函数的 f_locals = NULL),但module 的local 和global 指向是一样的,即全局名字空间,当然三者的global 指向肯定是一
致的,Python 所有线程都共享同样的 builtin 名字空间(也就是 __builtin__ module 所维护的dict)。f_code 指向对应的PyCodeObject 对象,
Frame 的调用链用f_back 连接起来。注意:f_locals 和 f_globals 都可能在运行时动态添加删除条目,假设函数g 定义在 f 之后,在执行 f() 时 函数对
象g 已经被创建产生并且被加入到 f_globals 中,于是可以在 f 中调用 g,这点与c 语言不同(基于函数出现位置)。
p140: 关于名字、作用域和名字空间。
关键词:静态作用域、LEGB(local, enclosing 闭包, global, builtin)查找原则(注意在module 内查找)、最内嵌套作用域原则
最内嵌套作用域原则:由一个赋值语句引进的名字在这个赋值语句所在的作用域里是可见的,而且在其内部嵌套的每个作用域内里也可见,除非它被嵌套于内部的,引进同样名字的另一条赋值语句所遮蔽。
global 表达式不受LEGB 原则约束;名字引用受LEGB原则约束;属性引用不受约束,可以简单理解为带 .的表达式,比如引用其
他模块的函数或变量 or 类的成员函数或class 变量引用。
p180: [COMPARE_OP] 代码段中第二个if 判断应该是JUMP_IF_TRUE
p185: PyFrameObject 中的 PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ ,PyTryBlock 在循环控制流和异
常处理流中被用于存储虚拟机进入流程前的一些状态信息,以便恢复到先前状态。
1
2 3 4 5 |
typedef
struct {
int b_type; /* what kind of block this is */ int b_handler; /* where to jump to find handler */ int b_level; /* value stack level to pop to */ } PyTryBlock; |
p193: while_control.py 的字节码中 a 修改为 i
p201: PyThreadState 对象是Python 为线程准备的在Python 虚拟机一级保存线程状态信息的对象,比如异常信息存放在 curexc_type、curexc_value
,通过 sys.exc_info()[0/1] 可以获取。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
typedef
struct _ts {
/* See Python/ceval.c for comments explaining most fields */ struct _ts *next; PyInterpreterState *interp; struct _frame *frame; int recursion_depth; /* 'tracing' keeps track of the execution depth when tracing/profiling. This is to prevent the actual trace/profile code from being recorded in the trace/profile. */ ... PyObject *curexc_type; PyObject *curexc_value; PyObject *curexc_traceback; PyObject *exc_type; PyObject *exc_value; PyObject *exc_traceback; PyObject *dict; /* Stores per-thread state */ ... PyObject *async_exc; /* Asynchronous exception to raise */ long thread_id; /* Thread id where this tstate was created */ } PyThreadState; |
p206: 异常产生的处理流程:
假如h() 函数内执行了一个 1/0 操作,那么在PyEval_EvalFrameEx() (注意此函数是被递归调用的,可以认为一个PyFrameObject 对
应一个函数)内switch 执行此字节码指令后发现产生产生除数为0的异常,便首先记录异常信息到 PyThreadState.curexc_type =
&ZeroDivisionError; PyThreadState.curexc_value = &"integer divison or modulo by zero"; 接着由指令执行结果返回NULL(进而why
== WHY_EXCEPTION) 得知产生异常,先创建 traceback 对象,进而在当前栈帧寻找 except 语句,以寻找开发人员指定的捕捉异
常的东西,如果没有找到,那么Python 虚拟机将退出当前的活动栈帧,并沿着栈帧链表向上回退到上一个栈帧(tstate->frame = f-
>f_back),这个沿着栈帧链不断回退的过程称之为栈帧展开,在展开的过程中,Python 虚拟机不断创建与各个栈帧对应的
traceback 对象,并将其链接成链表,如下图所示,注意,tstate->curexc_traceback 指向最新的 traceback 对象。如果没有在任何一
层设置异常捕捉代码,那么最后Python 虚拟机从线程状态对象中取出其维护的 traceback 对象,并遍历 traceback 对象链表,逐个输
出其中的信息,也就是我们所熟悉的 Traceback (most recent last call): ...
1
2 3 4 5 6 7 |
typedef
struct _traceback {
PyObject_HEAD struct _traceback *tb_next; struct _frame *tb_frame; int tb_lasti; //发生异常时执行的最后一行指令 int tb_lineno; // 发生异常时指令对应的源码行 } PyTracebackObject; |
那如果代码出现了 except or finally 语句呢,此时前面说过的 PyTryBlock 就出场了。对于循环控制流,其 b_type 为SETUP_LOOP,
而except 和 finally 分别是 SETUP_EXCEPT 和 SETUP_FINALLY,此时 b_handler 保存着 except or finally 语句编译后的第一条字节
码指令偏移地址。如果在发生异常后查找到 except or finally 语句,则虚拟机的状态由 WHY_EXCEPTION 转为 WHY_NOT(正常状
态),接下去就 JUMPTO(b->b_handler)。当然如果在当前栈帧查找到 except 语句但是异常类型不匹配,也会发起栈帧展开过程
(虚拟机状态变成WHY_RERAISE),即继续向上寻找,需要注意的是 finally 语句肯定是会执行的,即使当前栈帧的 except 语句类
型不匹配。
p217: 对于一段Python 函数代码,对应一个PyCodeObject 对象,如果对一个函数调用多次,则运行时创建多个PyFunctionObject 对
象,每个对象的func_code 都指向PyCodeObject,func_globals 赋值为当前活动 Frame 的f_globals,如果有默认参数值则存储在 func_defaults 中
(默认参数需要用不可变对象,否则运行时可能出现逻辑错误)。注意,使用dis.dis 查看时,函数f 的具体实现的字节码指令不会出现,因为它们是在
与函数f 对应的PyCodeObject 对象中。def f() 这条语句从语法上讲是函数声明语句,而从虚拟机实现角度看是函数对象的创建语句,即声明与定义分
离在不同PyCodeObject 对象中,类也是一样的,类定义中的函数同理。也就是说在执行 py 时,def f() or class A() 这样的语句实际上会创建函数对
象 or class 对象,并添加进 module 的 f_locals 中,当然类似 import sys 这样的操作也会添加 { 'sys' :
p226: 函数参数分为位置参数 f(a, b)、键参数 f(a, b,name="python")、扩展位置参数def f(a, b,*list)、扩展键参数 def f(a, b, **keys) 。
PyCodeObject 对象的 co_argcount 表示函数参数个数,co_nlocals 表示局部变量个数(包含co_argcount),在def 语句中出现的参数名称
都记录在变量名表co_varnames 中,它们都是函数编译后产生的PyCodeObject 对象中的域,它们的值只能在编译时确定,如 def f(a, b, *list): c = 1
则 co_argcount 为 2,co_nlocals 为4,即 *list 被当成 局部变量,**keys 同理。如下图函数调用过程中参数变化的情况,注意LOAD_FAST 与
STORE_FAST 之间执行了 age += 5 操作。
注:在最终通过PyEval_EvalFrameEx 时,PyFunctionObject 对象的影响已经消失了,真正对新栈帧产生影响的是 PyFunctionObject 输送的
PyCodeObject 对象和 global 名字空间,比如 PyFrame_New(tstate, co, globals, NULL) 产生新栈帧 PyFrameObject 对象,注意:传递给
PyFrame_New 的 global 参数是来自 PyFunctionObject.func_globals,这涉及到调用其他模块中定义的函数问题。Python 虚拟机在新栈帧环境中开始
一次执行新的字节码指令序列的循环,也就是函数所对应的字节码指令序列 PyCodeObject.co_code,新产生的Frame 的f_code 指向
此 PyCodeObject。
p232 & p239 & p244: 代码中 extras = f->f_nlocals + ncells + nfrees ; & freevars = f->f_localsplus + f->f_nlocals;
f->f_nlocals 修改为 co->co_nlocals; 其实Frame 也只有f_locals 。