《python解释器源码剖析》第11章--python虚拟机中的控制流

11.0 序

在上一章中,我们剖析了python虚拟机中的一般表达式的实现。在剖析一遍表达式是我们的流程都是从上往下顺序执行的,在执行的过程中没有任何变化。但是显然这是不够的,因为怎么能没有流程控制呢。下面我们来看看python所提供的流程控制手段,其中也包括异常检测机制。

11.1 python虚拟机中的if控制流

11.1.1 if字节码

if算是最简单也是最常用的控制流语句,我们看看它的字节码是怎么样的呢?

a = 1
if a > 10:
print("a > 10")
elif a <= -2:
print("a <= -2")
elif a != 1:
print("a != 1")
elif a == 1:
print("a == 1")
else:
print("unknown a")

上面的py文件文件,字节码如下。可以看到,字节码很长,我们后面慢慢来分析。

  • 第一行:源代码的行号
  • 第二行:字节码的偏移量,或者说指令索引,总之就是用来标记指令位置的
  • 第三行:指令
  • 第四行:指令参数,比如LOAD_CONST(0)表示获取索引为0的值,BUILD_LIST(2)表示创建容量为2的list
  • 第五行:指令参数对应的值,这个是用于观测的时候用于提示的,表示将指令参数传进去得到的值,正因为是提示,所以加上了个小括号
  1           0 LOAD_CONST               0 (1)
2 STORE_NAME 0 (a) 2 4 LOAD_NAME 0 (a)
6 LOAD_CONST 1 (10)
8 COMPARE_OP 4 (>)
10 POP_JUMP_IF_FALSE 22 3 12 LOAD_NAME 1 (print)
14 LOAD_CONST 2 ('a > 10')
16 CALL_FUNCTION 1
18 POP_TOP
20 JUMP_FORWARD 62 (to 84) 4 >> 22 LOAD_NAME 0 (a)
24 LOAD_CONST 3 (-2)
26 COMPARE_OP 1 (<=)
28 POP_JUMP_IF_FALSE 40 5 30 LOAD_NAME 1 (print)
32 LOAD_CONST 4 ('a <= -2')
34 CALL_FUNCTION 1
36 POP_TOP
38 JUMP_FORWARD 44 (to 84) 6 >> 40 LOAD_NAME 0 (a)
42 LOAD_CONST 0 (1)
44 COMPARE_OP 3 (!=)
46 POP_JUMP_IF_FALSE 58 7 48 LOAD_NAME 1 (print)
50 LOAD_CONST 5 ('a != 1')
52 CALL_FUNCTION 1
54 POP_TOP
56 JUMP_FORWARD 26 (to 84) 8 >> 58 LOAD_NAME 0 (a)
60 LOAD_CONST 0 (1)
62 COMPARE_OP 2 (==)
64 POP_JUMP_IF_FALSE 76 9 66 LOAD_NAME 1 (print)
68 LOAD_CONST 6 ('a == 1')
70 CALL_FUNCTION 1
72 POP_TOP
74 JUMP_FORWARD 8 (to 84) 11 >> 76 LOAD_NAME 1 (print)
78 LOAD_CONST 7 ('unknown a')
80 CALL_FUNCTION 1
82 POP_TOP
>> 84 LOAD_CONST 8 (None)
86 RETURN_VALUE

11.1.2 比较操作

我们字节码中源代码序号的右边有几个>>这样的符号,这是什么呢?仔细看一下应该就知道,这显然就是if后面的每一个分支开始的地方,当然为了区分开,最后的>>是返回值。

我们先看if

  1           0 LOAD_CONST               0 (1)  LOAD_CONST将1这个值load进来,压入运行时栈
2 STORE_NAME 0 (a) 从符号表将a load、从运行时栈弹出值,映射到local命名空间 2 4 LOAD_NAME 0 (a) LOAD_NAME表示获取a对应的值
6 LOAD_CONST 1 (10)LOAD_CONST直接将10这个常量load进来,前面的1代表10这个常量在co_consts中索引为1
8 COMPARE_OP 4 (>) 将上面获取的两个值进行比较操作,调用COMPARE_OP,参数是4,这个>是用于人性化显示的,表示将4传入COMPARE_OP得到的是>的比较操作
10 POP_JUMP_IF_FALSE 22 跳转

从if中我们可以看到大致的结构,至于elif、else是比较类似的。而且我们发现当中的COMPARE_OP也是支持传参的,比如if里面COMPARE_OP传入的是4,但是这个4代表什么呢?

// object.h
/* Rich comparison opcodes */
#define Py_LT 0 //小于
#define Py_LE 1 //小于等于
#define Py_EQ 2 //等于
#define Py_NE 3 //不等于
#define Py_GT 4 //大于
#define Py_GE 5 //大于等于 //opcode.h
enum cmp_op {PyCmp_LT=Py_LT, PyCmp_LE=Py_LE, PyCmp_EQ=Py_EQ, PyCmp_NE=Py_NE,
PyCmp_GT=Py_GT, PyCmp_GE=Py_GE, PyCmp_IN, PyCmp_NOT_IN,
PyCmp_IS, PyCmp_IS_NOT, PyCmp_EXC_MATCH, PyCmp_BAD};

COMPARE_OP指令

下面我们来看看,虚拟机中是如何进行比较操作的。另外本章中如果没有指定源码位置,那么默认是在ceval.c里面

static PyObject *
cmp_outcome(int op, PyObject *v, PyObject *w)
{
int res = 0;
switch (op) {
//python中的is
case PyCmp_IS:
res = (v == w);
break;
//python中的is not
case PyCmp_IS_NOT:
res = (v != w);
break;
//python中的in
case PyCmp_IN:
res = PySequence_Contains(w, v);
if (res < 0)
return NULL;
break;
//python中的not in
case PyCmp_NOT_IN:
res = PySequence_Contains(w, v);
if (res < 0)
return NULL;
res = !res;
break;
//python中的异常
case PyCmp_EXC_MATCH:
if (PyTuple_Check(w)) {
Py_ssize_t i, length;
length = PyTuple_Size(w);
for (i = 0; i < length; i += 1) {
PyObject *exc = PyTuple_GET_ITEM(w, i);
if (!PyExceptionClass_Check(exc)) {
PyErr_SetString(PyExc_TypeError,
CANNOT_CATCH_MSG);
return NULL;
}
}
}
else {
if (!PyExceptionClass_Check(w)) {
PyErr_SetString(PyExc_TypeError,
CANNOT_CATCH_MSG);
return NULL;
}
}
res = PyErr_GivenExceptionMatches(v, w);
break;
default:
//重点来了,这里这是比较操作,发现这里是富比较
/*
这个PyObject_RichCompare我们之前应该介绍过,这里就不看源码了,直接说结论。
首先调用这个方法,会进行检测确保比较的操作符一定是>、>=、<=、<、==、!=之间的一种
然后这个值的类型如果相同,并且不是用户自定义的类型,那么会选择其类型所对应PyTypeObject定义的tp_richcompare操作
如果没有tp_richcompare操作,那么就选择其对应的tp_compare操作。
因此无论是内建对象还是自定义的对象,其比较操作都是在其对应的类型对象的tp_richcompare或者tp_compare中
如果两个都没有,就会报错,出现一个TypeError: 'xx' not supported between instances of 'xx1' and 'xx2'
*/
return PyObject_RichCompare(v, w, op);
}
v = res ? Py_True : Py_False;
Py_INCREF(v);
return v;
}

比较操作的结果--python中的bool对象

在大多数编程语言中,比较操作的结果通常是一个bool值,即使没有bool值的C语言,也是用1和0来代替。python虚拟机中也有这样的两个对象:Py_True和Py_False。注意这两个也是对象,但是本质上也是PyLongObject

指令跳跃

  1           0 LOAD_CONST               0 (1)
2 STORE_NAME 0 (a) 2 4 LOAD_NAME 0 (a)
6 LOAD_CONST 1 (10)
8 COMPARE_OP 4 (>)
10 POP_JUMP_IF_FALSE 22 3 12 LOAD_NAME 1 (print)
14 LOAD_CONST 2 ('a > 10')
16 CALL_FUNCTION 1
18 POP_TOP
20 JUMP_FORWARD 62 (to 84) 4 >> 22 LOAD_NAME 0 (a)
24 LOAD_CONST 3 (-2)
26 COMPARE_OP 1 (<=)
28 POP_JUMP_IF_FALSE 40 5 30 LOAD_NAME 1 (print)
32 LOAD_CONST 4 ('a <= -2')
34 CALL_FUNCTION 1
36 POP_TOP
38 JUMP_FORWARD 44 (to 84) 6 >> 40 LOAD_NAME 0 (a)
42 LOAD_CONST 0 (1)
44 COMPARE_OP 3 (!=)
46 POP_JUMP_IF_FALSE 58 7 48 LOAD_NAME 1 (print)
50 LOAD_CONST 5 ('a != 1')
52 CALL_FUNCTION 1
54 POP_TOP
56 JUMP_FORWARD 26 (to 84) 8 >> 58 LOAD_NAME 0 (a)
60 LOAD_CONST 0 (1)
62 COMPARE_OP 2 (==)
64 POP_JUMP_IF_FALSE 76 9 66 LOAD_NAME 1 (print)
68 LOAD_CONST 6 ('a == 1')
70 CALL_FUNCTION 1
72 POP_TOP
74 JUMP_FORWARD 8 (to 84) 11 >> 76 LOAD_NAME 1 (print)
78 LOAD_CONST 7 ('unknown a')
80 CALL_FUNCTION 1
82 POP_TOP
>> 84 LOAD_CONST 8 (None)
86 RETURN_VALUE

如果第一个判断a > 10成立,那么会执行接下来的print语句,如果判断不成立,显然要跳到下一个elif a <= -2,所以这里有一个指令跳跃的动作。那么python虚拟机是如何完成指令跳跃的呢?关键就在一个名为PREDICT的宏里面

#if defined(DYNAMIC_EXECUTION_PROFILE) || USE_COMPUTED_GOTOS
#define PREDICT(op) if (0) goto PRED_##op
#else
#define PREDICT(op) \
do{ \
_Py_CODEUNIT word = *next_instr; \
opcode = _Py_OPCODE(word); \
if (opcode == op){ \
oparg = _Py_OPARG(word); \
next_instr++; \
goto PRED_##op; \
} \
} while(0)
#endif
#define PREDICTED(op) PRED_##op:

在python中,有一些字节码指令通常都是按照顺序出现的,通过上一个字节码指令直接预测下一个字节码指令是可能的。比如COMPARE_OP的后面通常都会紧跟着POP_JUMP_IF_TRUE或者POP_JUMP_IF_FALSE,这在上面的字节码中可以很清晰的看到。

为什么要有这样的一个预测功能,因为当字节码之间的指令搭配出现的概率非常高时,如果预测成功,能够省去很多无谓的操作,使得执行效率大幅提高

我们可以看到, PREDICTED(POP_JUMP_IF_FALSE);实际上就是检查下一条待处理的字节码是否是POP_JUMP_IF_FALSE。如果是,那么程序会直接跳转到PRED_POP_JUMP_IF_FALSE那里,如果将COMPARE_OP这个宏展开,可以看得更加清晰

if (*next_instr == POP_JUMP_IF_FALSE)
goto PRED_POP_JUMP_IF_FALSE;
if (*next_instr == POP_JUMP_IF_TRUE)
goto PRED_POP_JUMP_IF_TRUE

但是问题又来了,PRED_POP_JUMP_IF_TRUE和PRED_POP_JUMP_IF_FALSE这些标识在哪里呢?我们知道指令跳跃的目的是为了绕过一些无谓的操作,直接进入POP_JUMP_IF_TRUE或者POP_JUMP_IF_FALSE指令对应的case语句之前。

首先if a>10这条字节码序列中,存在POP_JUMP_IF_FALSE指令,那么在COMPARE_OP指令的实现代码的最后,将执行goto PRED_POP_JUMP_IF_FALSE;,而显然这句代码要在POP_JUMP_IF_FALSE之前执行

        PREDICTED(POP_JUMP_IF_FALSE);
TARGET(POP_JUMP_IF_FALSE) {
//取出之前比较的结果。
PyObject *cond = POP();
int err;
//比较结果为True,顺序执行
if (cond == Py_True) {
Py_DECREF(cond);
FAST_DISPATCH();
}
//比较结果为False,进行跳转
if (cond == Py_False) {
Py_DECREF(cond);
JUMPTO(oparg);
FAST_DISPATCH();
}
//异常检测
err = PyObject_IsTrue(cond);
Py_DECREF(cond);
if (err > 0)
;
else if (err == 0)
JUMPTO(oparg);
else
goto error;
DISPATCH();
}

我们看到调用跳转使用的JUMPTO,而这是一个宏

#define JUMPTO(x)       (next_instr = first_instr + (x) / sizeof(_Py_CODEUNIT))
/*
_Py_CODEUNIT 是 uint16_t 的别名 typedef uint16_t _Py_CODEUNIT,占两个字节;
*/

11.1.3 分析字节码

再来看看字节码

  1           0 LOAD_CONST               0 (1) //将1这个值load进来
2 STORE_NAME 0 (a) //将a这个变量load进来 2 4 LOAD_NAME 0 (a) //load变量a对应的值
6 LOAD_CONST 1 (10)//load 10这个值
8 COMPARE_OP 4 (>)//两者比较
10 POP_JUMP_IF_FALSE 22 //如果是POP_JUMP_IF_FALSE,那么跳转到PRED_POP_JUMP_IF_FALSE,显然是22 LOAD_NAME 3 12 LOAD_NAME 1 (print) //load print函数
14 LOAD_CONST 2 ('a > 10') //load一个字符串
16 CALL_FUNCTION 1 //函数调用
18 POP_TOP //从栈顶打印元素
20 JUMP_FORWARD 62 (to 84) //跳转到第84行指令,但显然这只是愿望,因为一旦走到这里,就代表这个条件是通过的,整个if else语句就结束了。但实际上,走到第10行的时候就跳转了 4 >> 22 LOAD_NAME 0 (a) //load 变量a对应的值
24 LOAD_CONST 3 (-2)//load -2
26 COMPARE_OP 1 (<=)//比较
28 POP_JUMP_IF_FALSE 40//跳到40 //下面都是一样的
5 30 LOAD_NAME 1 (print)
32 LOAD_CONST 4 ('a <= -2')
34 CALL_FUNCTION 1
36 POP_TOP
38 JUMP_FORWARD 44 (to 84) 6 >> 40 LOAD_NAME 0 (a)
42 LOAD_CONST 0 (1)
44 COMPARE_OP 3 (!=)
46 POP_JUMP_IF_FALSE 58 7 48 LOAD_NAME 1 (print)
50 LOAD_CONST 5 ('a != 1')
52 CALL_FUNCTION 1
54 POP_TOP
56 JUMP_FORWARD 26 (to 84) 8 >> 58 LOAD_NAME 0 (a)
60 LOAD_CONST 0 (1)
62 COMPARE_OP 2 (==)
64 POP_JUMP_IF_FALSE 76 //显然这里不为False了 9 66 LOAD_NAME 1 (print)
68 LOAD_CONST 6 ('a == 1')
70 CALL_FUNCTION 1
72 POP_TOP
74 JUMP_FORWARD 8 (to 84) 11 >> 76 LOAD_NAME 1 (print)
78 LOAD_CONST 7 ('unknown a')
80 CALL_FUNCTION 1
82 POP_TOP
>> 84 LOAD_CONST 8 (None) //最后load None
86 RETURN_VALUE //返回

11.2 python虚拟机中的for循环控制流

我们在if语句中已经见识了最基本的控制,但是我们发现if里面只能向前,不管是哪个分支,最终的结果都是JUMP_FORWARD。下面介绍for循环,我们会见到指令时可以回退的。但是在if中,我们看到指令的跳跃距离通常都是当前指令与目标指令的距离,相当于向前跳了多少步。那么指令回退时,是不是相当于向后跳了多少步呢?带着疑问,我们来往下看。

11.2.1 for字节码

我们来看看一个简单的for循环的字节码

lst = [1, 2]
for i in lst:
print(i)
1         0 LOAD_CONST               0 (1)
2 LOAD_CONST 1 (2)
4 BUILD_LIST 2
6 STORE_NAME 0 (lst) 2 8 SETUP_LOOP 20 (to 30)
10 LOAD_NAME 0 (lst)
12 GET_ITER
>> 14 FOR_ITER 12 (to 28)
16 STORE_NAME 1 (i) 3 18 LOAD_NAME 2 (print)
20 LOAD_NAME 1 (i)
22 CALL_FUNCTION 1
24 POP_TOP
26 JUMP_ABSOLUTE 14
>> 28 POP_BLOCK
>> 30 LOAD_CONST 2 (None)
32 RETURN_VALUE

11.2.2 循环结构的初始化

我们看看for循环的字节码是怎么样的

  1           0 LOAD_CONST               0 (1)
2 LOAD_CONST 1 (2)
4 BUILD_LIST 2
6 STORE_NAME 0 (lst) 第一行是都list的初始化,这个无需介绍。
直接load两个常量consts,然后build list,最后建立符号表和值的映射

关键是下面:

2         8 SETUP_LOOP              20 (to 30)
10 LOAD_NAME 0 (lst)
12 GET_ITER
>> 14 FOR_ITER 12 (to 28)
16 STORE_NAME 1 (i) 3 18 LOAD_NAME 2 (print)
20 LOAD_NAME 1 (i)
22 CALL_FUNCTION 1
24 POP_TOP
26 JUMP_ABSOLUTE 14
>> 28 POP_BLOCK
>> 30 LOAD_CONST 2 (None)
32 RETURN_VALUE 我们先忽略掉8 SETUP_LOOP和28 POP_BLOCK
当for i in lst:的时候,肯定首先要找到lst,所以指令是LOAD_NAME是没问题的
但是下面出现了GET_ITER,从字面上我们知道这是获取迭代器
其实即使不从源码的角度,我相信很多人对于for循环的机制也不是很了解。
实际上我们for循环遍历一个对象的时候,首先要满足后面的对象是一个可迭代对象
遍历这个对象的时候,会先调用这个对象的__iter__方法,把它变成一个迭代器
然后不断地调用这个迭代器的__next__方法,一步一步将里面的值全部迭代出来
直到没有可迭代元素,然后再进行一次迭代,出现StopIteration异常,for循环捕捉,然后退出。所以10个元素的迭代器,是需要迭代11次才能结束的。
所以for循环后面如果跟的是一个迭代器,那么直接调用__next__方法
如果是可迭代对象,会先调用其内部的__iter__方法,然后再调用__next__ 所以在GET_ITER后面出现了FOR_ITER,把里面的元素迭代出来,再STORE_NAME,这里就是我们的i
此时往下执行,load print和i,然后CALL_FUNCTION函数调用,当然这里调用的就是print
POP_TOP,从栈顶把元素打印出来,然后JUMP_ABSOLUTE,参数是14,表示跳转到字节码索引或者偏移量为14的地方。 14对应的字节码还是FOR_ITER,而不是GET_ITER,这也说明了只会调用一次GET_ITER。
当迭代完成之后,再次迭代已经没有元素了,我们看到了一个(to 30),表示结束直接跳到30、直接返回了。

PyTryBlock

我们来看看SETUP_LOOP,这里是for循环梦开始的地方。

//ceval.c
TARGET(SETUP_LOOP)
TARGET(SETUP_EXCEPT)
TARGET(SETUP_FINALLY) {
/* NOTE: If you add any new block-setup opcodes that
are not try/except/finally handlers, you may need
to update the PyGen_NeedsFinalizing() function.
*/ PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg,
STACK_LEVEL());
DISPATCH();
}
//我们看到仅仅是调用了一个PyFrame_BlockSetup函数
//frameobject.c
void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
PyTryBlock *b;
//这个f_iblock为f_blockstack上的索引
//CO_MAXBLOCKS是一个宏,为20
if (f->f_iblock >= CO_MAXBLOCKS)
Py_FatalError("XXX block stack overflow");
//这里我们算是真正意义上第一次使用栈帧中的f_blockstack属性
//这个f_blockstack显然是个PyTryBlock结构体实例
b = &f->f_blockstack[f->f_iblock++];
b->b_type = type;
b->b_level = level;
b->b_handler = handler;
}
//我们看看PyTryBlock长什么样
//frameobject.h
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;
/*
显然PyFrameObject对象中的f_blockstack是一个由PyTryBlock对象组成的数组,而SETUP_LOOP指令所做的就是从这个数组中获得了一块PyTryBlock结构,并在这个结构中存放了一些python虚拟机当前的状态信息。比如当前执行的字节码指令,当前运行时栈的深度等等。那么这个结构在for循环控制结构中起着什么样的作用呢?我们后面就会知晓 我们注意到PyTryBlock中有一个b_type域,注释写着这表示是那种的block,也就意味着存在着多种不同用途的PyTryBlock对象。从PyFrame_BlockSetup中可以看到,这个b_type实际上被设置为当前python虚拟机正在执行的字节码指令,以字节码指令作为区分PyTryBlock的不同用途
*/

list的迭代器

在SETUP_LOOP指令从PyFrameObject的f_blockstack中申请了一块PyTryBlock结构的空间之后,python虚拟机通过10 LOAD_NAME 0 (lst)指令,将刚创建的PyListObject对象压入运行时栈。然后再通过12 GET_ITER指令来获取PyListObject对象的迭代器。

        TARGET(GET_ITER) {
/* before: [obj]; after [getiter(obj)] */
//从运行时栈获取PyListObject对象
PyObject *iterable = TOP();
//获取该PyListObject对象的iterator
PyObject *iter = PyObject_GetIter(iterable);
Py_DECREF(iterable);
//将iterator压入栈中
SET_TOP(iter);
if (iter == NULL)
goto error;
PREDICT(FOR_ITER);
PREDICT(CALL_FUNCTION);
DISPATCH();
}

我们来看看PyObject_GetIter长什么样

//object.h
typedef PyObject *(*getiterfunc) (PyObject *);
//Objects/abstract.c
PyObject *
PyObject_GetIter(PyObject *o)
{
PyTypeObject *t = o->ob_type;
getiterfunc f; //获取类型对象中tp_iter操作
f = t->tp_iter;
if (f == NULL) {
if (PySequence_Check(o))
return PySeqIter_New(o);
return type_error("'%.200s' object is not iterable", o);
}
else {
//通过tp_iter获取iterator
PyObject *res = (*f)(o);
if (res != NULL && !PyIter_Check(res)) {
PyErr_Format(PyExc_TypeError,
"iter() returned non-iterator "
"of type '%.100s'",
res->ob_type->tp_name);
Py_DECREF(res);
res = NULL;
}
return res;
}
}

因此我们可以看到,PyObject_GetIter是调用对象对应的类型对象中的tp_iter操作来获取与对象关联的迭代器的。另外不光是PyListObject,只要是调用PyObject_GetIter获取其对应迭代器,那么这些迭代器也是一个实实在在的对象,很显然,一切皆对象。那么也必然会有对应的类型对象

//listobject.c
typedef struct {
PyObject_HEAD
Py_ssize_t it_index;
PyListObject *it_seq; /* Set to NULL when iterator is exhausted */
} listiterobject; PyTypeObject PyListIter_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"list_iterator", /* tp_name */
...
};

我们再来看看PyList_Type

PyTypeObject PyList_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"list",
sizeof(PyListObject),
...
list_iter, /* tp_iter */
...
};

我们看到PyList_Type中tp_iter域被设置为list_iter,显然这是PyObject_GetIter中的那个f,而这也正是创建迭代器的关键所在。

static PyObject *
list_iter(PyObject *seq)
{
listiterobject *it; if (!PyList_Check(seq)) {
PyErr_BadInternalCall();
return NULL;
}
//为listiterobject申请空间
it = PyObject_GC_New(listiterobject, &PyListIter_Type);
if (it == NULL)
return NULL;
//PyListObject对象中的索引
it->it_index = 0;
Py_INCREF(seq);
//这里的seq就是之前的PyListObject对象
it->it_seq = (PyListObject *)seq;
_PyObject_GC_TRACK(it);
return (PyObject *)it;
}

可以看到PyListObject的迭代器对象只是对PyListObject对象做了一个简单的包装,在迭代器中,维护了当前访问的元素在PyListObject对象中的序号:it_index。通过这个序号,listiterobject对象就可以实现PyListObject的遍历。

GET_ITER指令在获取了PyListObject对象的迭代器之后,使用SET_TOP宏强制将这个迭代器设置为运行时栈的栈顶元素。

《python解释器源码剖析》第11章--python虚拟机中的控制流

在指令GET_ITER完成之后,python虚拟机开始了FOR_ITER指令的预测动作,如你所知,这样的预测动作是为了提高执行的效率。

11.2.3 迭代控制

源代码中的for循环,在虚拟机层面也一定对应着一个相应的循环控制结构。因为无论进行怎样的变换,都不可能在虚拟机层面利用顺序结构来实现源码层面上的循环结构,这也可以看成是程序的拓扑不变性。显然正如我们刚才分析的,当创建完迭代器之后,就正式开始进入for循环了,没错就是从FOR ITER开始,进入了python虚拟机层面上的for循环

        PREDICTED(FOR_ITER);
TARGET(FOR_ITER) {
/* 从栈顶获取iterator对象 */
PyObject *iter = TOP();
//从iteator中获取下一个元素
PyObject *next = (*iter->ob_type->tp_iternext)(iter);
if (next != NULL) {
//如果next不为NULL,那么压入运行时栈
PUSH(next);
PREDICT(STORE_FAST);
PREDICT(UNPACK_SEQUENCE);
DISPATCH();
}
if (PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_StopIteration))
goto error;
else if (tstate->c_tracefunc != NULL)
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
PyErr_Clear();
}
/* 走到这里说明迭代正常结束 */
STACKADJ(-1);
Py_DECREF(iter);
JUMPBY(oparg);
PREDICT(POP_BLOCK);
DISPATCH();
}

FOR_ITER的指令代码会首先从运行时栈中获得PyListObject对象的迭代器,然后调用迭代器的tp_iternext开始进行迭代,得到的结果总是返回迭代器对象中的下一个元素。如果抵达了迭代器的结束位置,那么tp_iternext将返回NULL,这个结果预示着遍历结束。

FOR_ITER的指令代码会检查tp_iternext的返回结果,如果得到的是一个有效的元素(x!=NULL),那么将获得的这个元素压入到运行时栈中,并开始进行一系列的字节码预测动作。在我们当前的例子中,显然会预测失败,因此会执行STORE_NAME

那么如何获取迭代器的下一个元素呢?

//listobject.c
static PyObject *
listiter_next(listiterobject *it)
{
PyListObject *seq;
PyObject *item; assert(it != NULL);
//seq:显然是获取迭代器对象的PyListObject对象
seq = it->it_seq;
if (seq == NULL)
return NULL;
assert(PyList_Check(seq)); if (it->it_index < PyList_GET_SIZE(seq)) {
//获得序号或者索引为it_index的元素对戏那个
item = PyList_GET_ITEM(seq, it->it_index);
//调整index,使其指向下一个元素
++it->it_index;
Py_INCREF(item);
return item;
} //迭代完毕之后,设置为NULL,所以迭代器只能够顺序迭代一次
it->it_seq = NULL;
Py_DECREF(seq);
return NULL;
}

之后python虚拟机将沿着字节码的顺序一条一条的执行下去,从而完成输出的动作。但是我们知道,for循环中肯定会有指令回退的动作,我们之前从字节码中也看到了,for循环遍历一次之后,会再次跳转到FOR_ITER,而跳转所使用的指令就是JUMP_ABSOLUTE

        PREDICTED(JUMP_ABSOLUTE);
TARGET(JUMP_ABSOLUTE) {
JUMPTO(oparg);
#if FAST_LOOPS
FAST_DISPATCH();
#else
DISPATCH();
#endif
} #define JUMPTO(x) (next_instr = first_instr + (x) / sizeof(_Py_CODEUNIT))

可以看到和if不一样,for循环使用的是绝对跳跃。JUMP_ABSOLUTE是强制设置next_instr的值,将next_instr设定到距离f->f_code->co_code开始地址的某一特定偏移的位置。这个偏移的量由JUMP_ABSOLUTE的指令参数决定,所以这条参数就成了for循环中指令回退动作的最关键的一点。

2         8 SETUP_LOOP              20 (to 30)
10 LOAD_NAME 0 (lst)
12 GET_ITER
>> 14 FOR_ITER 12 (to 28)
16 STORE_NAME 1 (i) 3 18 LOAD_NAME 2 (print)
20 LOAD_NAME 1 (i)
22 CALL_FUNCTION 1
24 POP_TOP
26 JUMP_ABSOLUTE 14
>> 28 POP_BLOCK
>> 30 LOAD_CONST 2 (None)
32 RETURN_VALUE

我们看到参数是14,表示跳转到14 FOR_ITER这条指令,那么python虚拟机的下一步动作就是执行FOR_ITER指令,即通过PyListObject对象的迭代器获取PyListObject对象中的下一个元素,然后依次向前,执行输出。因此FOR_ITER指令和JUMP_ABSOLUTE指令之间构造出了一个循环结构,这个循环结构正是对应源码中的for循环结构。

但是我们发现,FOR_ITER后面跟了一个参数,这里是12,但是目前为止我们并没有看到有地方使用了这个12。其实,聪明如你肯定能猜到,因为从后面(to 28)也能看到,这是用于终止迭代的。

11.2.4 终止迭代

"天下没有不散的宴席",for循环也是要退出的,不用想这个退出的动作只能落在FOR_ITER的身上。在FOR_ITER指令执行的过程中,如果通过PyListObject对象的迭代器获取的下一个元素不是有效的元素(会是NULL),这就意味着迭代结束了。这个结果将直接导致python虚拟机会将迭代器对象从运行时栈中弹出,同时执行一个JUMPBY的动作,向前跳跃,在字节码的层面上是向下,就是字节码偏移量增大的方向。

#define JUMPBY(x)       (next_instr += (x) / sizeof(_Py_CODEUNIT))

这里的x是12,我们之前说_Py_CODEUNIT占两个字节,因此结果是6,表示将栈指针向前偏移6个指令,偏移6个之后,正好落在了POP_BLOCK指令上

        PREDICTED(POP_BLOCK);
TARGET(POP_BLOCK) {
//将运行时栈恢复为迭代前的状态
PyTryBlock *b = PyFrame_BlockPop(f);
UNWIND_BLOCK(b);
DISPATCH();
} //frameobject.c
PyTryBlock *
PyFrame_BlockPop(PyFrameObject *f)
{
PyTryBlock *b;
if (f->f_iblock <= 0)
Py_FatalError("XXX block stack underflow");
//向f_blockstack总归还PyTryBlock
b = &f->f_blockstack[--f->f_iblock];
return b;
}

前面我们知道,在SETUP_LOOP处,python虚拟机从f->f_blockstack中申请了一个PyTryBlock结果,并将执行SETUP_LOOP时的一些关于虚拟机的状态信息保存到了所获得的PyTryBlock结构中。在执行POP_BLOCK指令时,实际上是将PyTryBlock结构归还给了f->f_blockstack。同时,python虚拟机抽取在SETUP_LOOP指令处、保存在PyTryBlock中的信息,并根据其中存储的SETUP_LOOP指令处运行时栈的深度信息,将运行时栈恢复到SETUP_LOOP之前的状态,从而完成了整个for循环结构

在执行SETUP_LOOP指令时,python虚拟机保存了很多的信息,而在执行POP_BLOCK指令时,却只使用了栈的深度信息来恢复运行时栈,为什么会存在这种不对称呢?其实很简单,那么因为PyTryBlock并不是专门为for循环准备的,python中还有一些机制会使用到这个结构,为了避免代码过于复杂,python不管三七二十一,在PyFrame_BlockSetup中会一股脑将所有机制可能用到的参数全部存在PyTryBlock对象找那个,各个机制需要什么样的参数直接去取即可,对于其他参数则不需要理会。

11.3 python虚拟机中的while循环控制结构

一般的主流语言中都有:if、for、while、switch这四种结构,但是对于python来说,没有switch,因为完全可以使用if、字典来代替,因此就只剩下while了。

11.3.1 while字节码

会了if、for,那么再来看while就简单了。当然不仅如此,我们还要分析两个关键字:break、continue,当然goto就别想了。

a = 0
while a < 10:
a += 1
if a == 5:
continue
if a == 7:
break
print(a)
  1           0 LOAD_CONST               0 (0)
2 STORE_NAME 0 (a) 2 4 SETUP_LOOP 48 (to 54)
>> 6 LOAD_NAME 0 (a)
8 LOAD_CONST 1 (10)
10 COMPARE_OP 0 (<)
12 POP_JUMP_IF_FALSE 52 3 14 LOAD_NAME 0 (a)
16 LOAD_CONST 2 (1)
18 INPLACE_ADD
20 STORE_NAME 0 (a) 4 22 LOAD_NAME 0 (a)
24 LOAD_CONST 3 (5)
26 COMPARE_OP 2 (==)
28 POP_JUMP_IF_FALSE 32 5 30 JUMP_ABSOLUTE 6 6 >> 32 LOAD_NAME 0 (a)
34 LOAD_CONST 4 (7)
36 COMPARE_OP 2 (==)
38 POP_JUMP_IF_FALSE 42 7 40 BREAK_LOOP 8 >> 42 LOAD_NAME 1 (print)
44 LOAD_NAME 0 (a)
46 CALL_FUNCTION 1
48 POP_TOP
50 JUMP_ABSOLUTE 6
>> 52 POP_BLOCK
>> 54 LOAD_CONST 5 (None)
56 RETURN_VALUE

我们看到while的字节码基本上就是if和for字节码的组合,SETUP_LOOP处,虚拟机从当前活动的PyFrameObject对象中申请了一块PyTryBlock空间,并填入一些当前虚拟机的状态,然后正式开始进入while循环。所以我们下面只看while循环中运行时栈和local命名空间的变化情况

11.3.2 循环终止

当走到了10 COMPARE_OP 0的时候,此时开始比较,a和10都load进来了,此时运行时栈和局部变量表如图所示: 

《python解释器源码剖析》第11章--python虚拟机中的控制流

接着COMPARE_OP 0指令将执行小于比较操作,并将比较的结果存放到运行时栈中。然后下面就是if逻辑了,但是区别是多个if都要执行,如果是if`···elif···else的话则只会执行一次,这一点需要注意,至于逻辑和if基本是一致的。当循环不满足条件时,那么显然a=11,和10比较

《python解释器源码剖析》第11章--python虚拟机中的控制流

此时比较的结果为Py_False,所以指令直接跳跃到偏移量为52的地方执行POP_TOPPy_False弹出运行时栈,随后虚拟机通过执行POP_BLOCK指令销毁PyTryBlock对象

11.3.3 continue

在循环中,我们在a==5这个条件满足的时候执行了一个continue的动作,显然此时程序不会执行continue下面的代码,而是直接跳转到循环开始的地方。

  1           0 LOAD_CONST               0 (0)
2 STORE_NAME 0 (a) 2 4 SETUP_LOOP 48 (to 54)
>> 6 LOAD_NAME 0 (a)
8 LOAD_CONST 1 (10)
10 COMPARE_OP 0 (<)
12 POP_JUMP_IF_FALSE 52 3 14 LOAD_NAME 0 (a)
16 LOAD_CONST 2 (1)
18 INPLACE_ADD
20 STORE_NAME 0 (a) 4 22 LOAD_NAME 0 (a)
24 LOAD_CONST 3 (5)
26 COMPARE_OP 2 (==)
28 POP_JUMP_IF_FALSE 32 5 30 JUMP_ABSOLUTE 6

从字节码可以看出,如果28 POP_JUMP_IF_FALSE 32判断确实为Py_False,那么直接跳到字节码偏移量为32的地方,也就是下一个if条件语句,但是当a==5的时候,POP_JUMP_IF_FALSE判断结果为Py_True,那么这里就不会发生跳跃,而是会直接执行下面的30 JUMP_ABSOLUTE 6,这个指令我们见过的,绝对跳跃,直接跳到偏移量为6的地方,也就是while循环开始的位置,即进行下一次while循环

11.3.4 break

显然当a==7的时候,执行了break语句

  6     >>   32 LOAD_NAME                0 (a)
34 LOAD_CONST 4 (7)
36 COMPARE_OP 2 (==)
38 POP_JUMP_IF_FALSE 42 7 40 BREAK_LOOP 8 >> 42 LOAD_NAME 1 (print)
44 LOAD_NAME 0 (a)
46 CALL_FUNCTION 1
48 POP_TOP
50 JUMP_ABSOLUTE 6
>> 52 POP_BLOCK
>> 54 LOAD_CONST 5 (None)
56 RETURN_VALUE

显然当a==7的时候,如果38 POP_JUMP_IF_FALSE 42Py_False,那么会跳到42,准备打印工作。但是如果为Py_True则执行执行break语句,也就是这里的BREAK_LOOP

        TARGET(BREAK_LOOP) {
why = WHY_BREAK;
goto fast_block_end;
}

python虚拟机将我们之前提到的结束状态why设置为WHY_BREAK,然后进入fast_block_end标志符对应的代码处。这段代码比较复杂,因为还包含了与异常机制有关的代码,这里我们只截取与break相关的代码。

fast_block_end:
while (why != WHY_NOT && f->f_iblock > 0) {
/* 取得与当前while循环对应的PyTryBlock */
PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1];
f->f_iblock--;
UNWIND_BLOCK(b);
if (b->b_type == SETUP_LOOP && why == WHY_BREAK) {
why = WHY_NOT;
JUMPTO(b->b_handler);
break;
} } /* main loop */

python虚拟机首先获得之前通过SETUP_LOOP指令申请得到的、与当前while循环对应的PyTryBlock结构,将why值设置为WHY_NOT,表示退出时没有任何错误。再通过JUMPTO这个宏,将下一条指令的next_instr设置为距离code开始b -> b_handler 个字节的指令。而b -> b_handler的值是INSTR_OFFSET() + oparg

#define INSTR_OFFSET()  (sizeof(_Py_CODEUNIT) * (int)(next_instr - first_instr))

2 4 SETUP_LOOP 48 (to 54),里面的48就是oparg。而next_instr是当前指令的下一条指令,显然是第4条,first_instr是第一条指令,那么next_instr - first_instr结果为3,sizeof(_Py_CODEUNIT)我们之前说过是2,其实这也是字节码的大小,一条字节码是占两个字节的。只不过也是从0开始,所以我们除了称之为偏移量、有时候也会将叫成索引。最终加起来的结果是54,可以看到直接跳到了54 LOAD_CONST 5 (None),这也证明while循环确实结束了。不过至于这里的54 LOAD_CONST 5 (None)跟while循环就没有关系了,不管函数、类、还是模块,在结尾处都会有一个LOAD_CONST和RETURN_VALUE

11.4 python虚拟机中的异常控制流

程序在运行的过程中经常会遇到大量的错误,而python中也定义了大量的异常类型供我们使用,下面我们来看看python中的异常机制,因为这也是一个控制语句。

11.4.1 python中的异常机制

python虚拟机自身抛出异常

python有一套内建的异常捕捉机制,即使在python的脚本文件中没有出现try语句,python脚本执行出现的异常还是会被虚拟机捕捉到。首先我们就从ZeroDivisionError这个异常来分析

1 / 0

0 LOAD_CONST               0 (1)
2 LOAD_CONST 1 (0)
4 BINARY_TRUE_DIVIDE

我们看第3条字节码,异常也正是在执行这条字节码的时候触发的

        TARGET(BINARY_TRUE_DIVIDE) {
//co_consts(0, 1)
//首先这里拿到POP拿到1,TOP拿到0
PyObject *divisor = POP();//1
PyObject *dividend = TOP();//0
PyObject *quotient = PyNumber_TrueDivide(dividend, divisor);
Py_DECREF(dividend);
Py_DECREF(divisor);
SET_TOP(quotient);
//如果这里返回NULL,那么就直接抛出异常了
if (quotient == NULL)
goto error;
DISPATCH();
}

逻辑很简单, 就是获取两个值,然后调用PyNumber_TrueDivide相除,正常情况下得到的肯定是一个数值,如果不能相除那么就返回NULL,如果接受的quotient是NULL,那么抛异常。因此我们来看看PyNumber_TrueDivide都干了些啥?

//longobject.c
//最终调用的是long_true_divide
//代码很长我们截取一部分
static PyObject *
long_true_divide(PyObject *v, PyObject *w)
{
PyLongObject *a, *b, *x;
Py_ssize_t a_size, b_size, shift, extra_bits, diff, x_size, x_bits;
digit mask, low;
int inexact, negate, a_is_small, b_is_small;
double dx, result; CHECK_BINOP(v, w);
//将v和w中维护的整数值转存到a和b中
a = (PyLongObject *)v;
b = (PyLongObject *)w;
a_size = Py_ABS(Py_SIZE(a));
b_size = Py_ABS(Py_SIZE(b));
negate = (Py_SIZE(a) < 0) ^ (Py_SIZE(b) < 0);
//获取b_size,就是ob_size,我们在分析PyLongObject对象时说过
//如果这个对象维护的值为0,那么ob_size就是0,这是个特殊情况
//并且这个ob_size还可以体现出维护的值的正负
if (b_size == 0) {
PyErr_SetString(PyExc_ZeroDivisionError,
"division by zero");
goto error;
}
...
...
}

我们发现如果除以0,那么直接设置异常信息。另外我们说过python中一切皆对象,那么异常也是一个对象,是PyObject *类型

//pyerrors.h
typedef struct {
PyException_HEAD
} PyBaseExceptionObject; //BaseException,所有异常的基类 typedef struct {
PyException_HEAD
PyObject *msg;
PyObject *filename;
PyObject *lineno;
PyObject *offset;
PyObject *text;
PyObject *print_file_and_line;
} PySyntaxErrorObject; typedef struct {
PyException_HEAD
PyObject *msg;
PyObject *name;
PyObject *path;
} PyImportErrorObject; typedef struct {
PyException_HEAD
PyObject *encoding;
PyObject *object;
Py_ssize_t start;
Py_ssize_t end;
PyObject *reason;
} PyUnicodeErrorObject; typedef struct {
PyException_HEAD
PyObject *code;
} PySystemExitObject; typedef struct {
PyException_HEAD
PyObject *myerrno;
PyObject *strerror;
PyObject *filename;
PyObject *filename2;
#ifdef MS_WINDOWS
PyObject *winerror;
#endif
Py_ssize_t written; /* only for BlockingIOError, -1 otherwise */
} PyOSErrorObject; typedef struct {
PyException_HEAD
PyObject *value;
} PyStopIterationObject;

在线程状态对象中记录异常信息

我们之前看到,异常信息是通过PyErr_SetString(异常类型, 异常信息)来设置的,而除了这个PyErr_SetString,还会经过PyErr_SetObject,最终到达PyErr_Restore。在PyErr_Restore中,python将这个异常放置到了一个安全的地方。

//errors.c
void
PyErr_Restore(PyObject *type, PyObject *value, PyObject *traceback)
{
//获取当前的活动线程
PyThreadState *tstate = PyThreadState_GET();
PyObject *oldtype, *oldvalue, *oldtraceback; if (traceback != NULL && !PyTraceBack_Check(traceback)) {
/* XXX Should never happen -- fatal error instead? */
/* Well, it could be None. */
Py_DECREF(traceback);
traceback = NULL;
} //保存以前的异常信息
oldtype = tstate->curexc_type;
oldvalue = tstate->curexc_value;
oldtraceback = tstate->curexc_traceback; //设置当前的异常信息
tstate->curexc_type = type;
tstate->curexc_value = value;
tstate->curexc_traceback = traceback; //丢弃以前的异常信息
Py_XDECREF(oldtype);
Py_XDECREF(oldvalue);
Py_XDECREF(oldtraceback);
}

最后再PyThreadState的curexc_type中存下了PyExc_ZeroDivisionError,而cur_value中存下了字符串division by zero

我们再来看看PyThreadState对象,这个之前说了是与线程有关的,但是它只是对象线程信息的一个抽象描述,而真实的线程及状态肯定是由操作系统来维护和管理的,但是python虚拟机在运行的时候总需要另外一些与线程相关的状态和信息,比如是否发生了异常等等。而这些信息显然操作系统是没有办法提供的,而PyThreadState对象正是python为线程准备的在虚拟机层面保存保存线程状态信息的对象。在这里,当前活动线程对应的PyThreadState对象可以通过PyThreadState_GET获得,在得到了线程状态对象之后,就将异常信息存放到线程状态对象中。

python的sys模块提供了一个接口,让我们获取相关的信息

try:
1/ 0
except ZeroDivisionError:
import sys
exc_type, exc_value, exc_tb = sys.exc_info()
# tstate->curexc_type
print(exc_type)
# tstate->curexc_value
print(exc_value)
# tstate->curexc_traceback
print(exc_tb)
"""
<class 'ZeroDivisionError'>
division by zero
<traceback object at 0x0000022BA9EDF880>
"""
# 可以看到,这三个信息就是线程设置的相关信息
# 至于这个traceback,可以通过import trackback模块查看,主要是对异常进行回溯的。

展开栈帧

首先我们知道异常已经被记录在了线程的状态中了,现在可以回头看看,在跳出了分派字节码指令的switch块之后,发生了什么动作。而且这里还存在一个问题,那就是导致跳出那个巨大的switch块的原因可以是执行完了所有的字节码之后正常跳出,也可以是发生异常后跳出,那么python虚拟机到底如何区分这是哪一种呢?

PyObject* _Py_HOT_FUNCTION _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
for (;;) {
switch (opcode) {
// 一个超大的switch语句
}
assert(why == WHY_NOT);
// 设置why,告诉虚拟机, 异常发生了
why = WHY_EXCEPTION; // 尝试捕捉异常
if (why != WHY_NOT)
break; // 创建traceback对象
PyTraceBack_Here(f); if (tstate->c_tracefunc != NULL)
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj,
tstate, f);
...
}
}

在跳出了switch之后,如果why = WHY_NOT,那么表示正常退出,如果why = WHY_EXCEPTION,表示字节码在执行的过程中出现异常了。注意:当出现异常的时候,python虚拟机才获取了这个信息,也就是在执行过程中发生了异常。其实实际上这个变量why维护的就是python虚拟机执行字节码指令的那个for循环内的状态。

那么问题就来了, 如果在在涉及到函数调用的时候发生了异常该怎么办呢?首先在python虚拟机意识到有异常发生后,它就要开始进入异常处理的流程,这个流程会涉及到我们介绍PyFrameObject对象时所提到的那个PyFrameObject对象链表。在介绍PyFrameObject对象的时候,我们说过PyFrameObject实际上就是对栈帧的模拟,当发生函数函数调用,python会新创建一个栈帧(也叫PyFrameObject,这里就不做区分了),并将其内部的f_back连接到调用者对应的PyFrameObject,这样就形成了一条栈帧链。

def h():
1 / 0 def g():
h() def f():
g() f()

《python解释器源码剖析》第11章--python虚拟机中的控制流

Traceback (most recent call last):
File "C:/Users/satori/Desktop/love_minami/b.py", line 10, in <module>
f()
File "C:/Users/satori/Desktop/love_minami/b.py", line 8, in f
g()
File "C:/Users/satori/Desktop/love_minami/b.py", line 5, in g
h()
File "C:/Users/satori/Desktop/love_minami/b.py", line 2, in h
1 / 0
ZeroDivisionError: division by zero

这是脚本运行时产生的输出,我们看到了函数调用的信息:比如在源代码的哪一行调用了哪一个函数,那么这些信息是从何而来的呢?而且我们发现输出的信息是一个链状的结构,是不是和栈帧链比较相似啊。没错,在python虚拟机处理异常的时候,涉及到了一个traceback对象,在这个对象中记录栈帧链表的信息,python虚拟机利用这个对象来将栈帧链表中的每一个栈帧的状态进行可视化,这个可视化的结果就是上面输出的异常信息。

回到我们的例子,当异常发生时,当前活动的栈帧是函数h对应的栈帧。在python虚拟机开始处理异常的时候,它首先的动作就是创建一个traceback对象,用于记录异常发生时活动栈帧的状态。

PyObject* _Py_HOT_FUNCTION _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
for (;;) {
switch (opcode) {
// 一个超大的switch语句
}
// 创建traceback对象
PyTraceBack_Here(f); //这里tstate还是我们之前提到的与当前活动线程对应的线程对象
//其中的c_tracefunc是用户自定义的追踪函数,主要用于编写python的debugger。
//但是通常情况下这个值都是NULL,所以不考虑它。
//我们下面主要看PyTraceBack_Here(f),它到底使用PyFrameObject对象创建了一个怎样的traceback
if (tstate->c_tracefunc != NULL)
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj,
tstate, f);
...
}
}
//traceback.c
int
PyTraceBack_Here(PyFrameObject *frame)
{
PyObject *exc, *val, *tb, *newtb;
//获取线程中保存线程状态的traceback对象
PyErr_Fetch(&exc, &val, &tb);
//创建新的traceback对象
newtb = tb_create_raw((PyTracebackObject *)tb, frame, frame->f_lasti,
PyFrame_GetLineNumber(frame));
if (newtb == NULL) {
_PyErr_ChainExceptions(exc, val, tb);
return -1;
}
//将新的traceback对象交给线程状态对象
PyErr_Restore(exc, val, newtb);
Py_XDECREF(tb);
return 0;
}

原来traceback对象是保存在线程状态对象之中的,我们来看看这个traceback对象究竟长得什么样:

//traceback.h
typedef struct _traceback {
PyObject_HEAD
struct _traceback *tb_next;
struct _frame *tb_frame;
int tb_lasti;
int tb_lineno;
} PyTracebackObject;

可以看到里面有一个tb_next,所以很容易想到这个traceback对象也是一个链表结构。其实这个PyTracebackObject对象的链表结构应该跟PyFrameObject对象的链表结构是同构的、或者说一一对应的,即一个PyFrameObject对象应该对应一个PyTracebackObject对象。我们看看这个链表是怎么产生的:

//traceback.c
static PyObject *
tb_create_raw(PyTracebackObject *next, PyFrameObject *frame, int lasti,
int lineno)
{
PyTracebackObject *tb;
if ((next != NULL && !PyTraceBack_Check(next)) ||
frame == NULL || !PyFrame_Check(frame)) {
PyErr_BadInternalCall();
return NULL;
}
//申请内存,创建对象
tb = PyObject_GC_New(PyTracebackObject, &PyTraceBack_Type);
if (tb != NULL) {
//建立链表
Py_XINCREF(next);
//这里的next就是下一个PyTracebackObject
tb->tb_next = next;
Py_XINCREF(frame);
//设置栈帧
tb->tb_frame = frame;
//最后执行完毕的字节码偏移量
tb->tb_lasti = lasti;
//源代码行号
tb->tb_lineno = lineno;
PyObject_GC_Track(tb);
}
return (PyObject *)tb;
}

从源码中我们看到,tb_next是将两个traceback连接了起来,不过这个和PyFrameObject里面f_back正好相反。f_back指向的是上一个栈帧,而tb_next指向的是下一个traceback。另外在新创建的对象中,还使用tb_frame和对应的PyFrameObject对象建立了联系,当然还有最后执行完毕的字节码偏移量以及其在源代码中对应的行号。话说还记得PyCodeObject对象中的那个co_lnotab吗,这里的tb_lineno就是通过co_lnotab获取的。

python虚拟机意识到有异常抛出,并创建了traceback对象之后,它会在当前栈帧中寻找except语句,来执行开发人员指定的捕捉异常的动作,如果没有找到,那么python虚拟机将退出当前的活动栈帧,并沿着栈帧链回退到上一个栈帧,就像我们之前说的,出现函数调用会创建栈帧,当函数执行完毕或者出现异常的时候,会回退到上一级栈帧。一层一层创建、一层一层返回。至于回退的这个动作,则是在PyEval_EvalFrameEx的最后完成,当然准确的说应该是其内部调用的_PyEval_EvalFrameDefault的最后

PyObject* _Py_HOT_FUNCTION _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
for (;;) {
switch (opcode) {
// 大大的switch
}
// 告诉虚拟机, 异常发生了
why = WHY_EXCEPTION; // 尝试捕捉异常
if (why != WHY_NOT)
break; // 创建traceback对象
PyTraceBack_Here(f); if (tstate->c_tracefunc != NULL)
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj,
tstate, f);
//上面的都不变
...
}
//关键是这里,利用retval通知前一个栈帧有异常出现
if (why != WHY_RETURN)
retval = NULL; exit_eval_frame:
if (PyDTrace_FUNCTION_RETURN_ENABLED())
dtrace_function_return(f);
Py_LeaveRecursiveCall();
f->f_executing = 0;
// 将线程状态对象中的活动栈帧设置为上一个栈帧, 完成栈帧回退的动作
tstate->frame = f->f_back; return _Py_CheckFunctionResult(NULL, retval, "PyEval_EvalFrameEx");
}

可以看到,如果开发人员没有任何的捕获异常的动作,那么程序执行到代码中的if (why != WHY_NOT)处,这里也是虚拟机主for循环的结尾处,由于异常没有捕捉到,那么why的值仍然是WHY_EXCEPTION,那么将通过break跳出python执行字节码的那个for循环。最后,由于没有捕获到异常, 其返回值被设置为NULL,同时通过重新设置当前线程状态对象中的活动栈帧,设置为上一级栈帧,从而完成栈帧回退的动作。

此时我们的例子就很好解释了,当虚拟机执行函数f时,它是在PyEval_EvalFrameEx中执行与f对应的PyFrameObject对象中的字节码指令序列。当在函数f中调用g时,python虚拟机又会为函数g创建新的PyFrameObject对象,会把控制权交给函数g对应的PyFrameObject,当然调用的也是PyEval_EvalFrameEx,只不过这次是在执行与g对应的PyFrameObject对象中的字节码指令序列了。同理函数g调用函数h的时候,也是一样的。所以当在函数h中发生异常,没有异常捕获、导致PyEval_EvalFrameEx结束时,自然要返回到、或者把控制权再交给与函数g对应的PyFrameObject,由PyEval_EvalFrameEx继续执行。由于在返回时,retval被设置为NULL,所以回到g中,python虚拟机再次意识到有异常产生,可由于函数g中调用的时候也没有异常捕获,那么同样也要退出,再把PyEval_EvalFrameEx执行栈帧的控制权交给函数f对应的栈帧,如果还没有异常捕获,那么回到py文件对应的栈帧,再没有的话就直接报错了。

这个沿着栈帧链不断回退的过程我们称之为栈帧展开,在这个栈帧展开的过程中,python虚拟机不断地创建与各个栈帧对应的traceback对象,并将其链接成链表

《python解释器源码剖析》第11章--python虚拟机中的控制流

由于我们没有设置任何的异常捕获的代码,那么python虚拟机的执行流程会一直返回到PyRun_SimpleFileExFlags中,这个PyRun_SimpleFileExFlags是干啥的我们先不管,以后分析python运行时候的初始化时,就可以看到这个函数的作用了。

//pythonrun.c
int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
PyCompilerFlags *flags)
{ ....
//这个v是PyRun_FileExFlags的返回值
v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
closeit, flags);
if (v == NULL) {
Py_CLEAR(m);
PyErr_Print();
goto done;
}
ret = 0;
return ret;
} PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
PyObject *locals, int closeit, PyCompilerFlags *flags)
{ ...
//调用了run_mod
ret = run_mod(mod, filename, globals, locals, flags, arena); return ret;
} static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
PyCompilerFlags *flags, PyArena *arena)
{
...
//调用了PyEval_EvalCode
v = PyEval_EvalCode((PyObject*)co, globals, locals);
return v;
} //ceval.c
PyObject *
PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals)
{
//调用了PyEval_EvalCodeEx
return PyEval_EvalCodeEx(co,
globals, locals,
(PyObject **)NULL, 0,
(PyObject **)NULL, 0,
(PyObject **)NULL, 0,
NULL, NULL);
} PyObject *
PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
PyObject *const *args, int argcount,
PyObject *const *kws, int kwcount,
PyObject *const *defs, int defcount,
PyObject *kwdefs, PyObject *closure)
{
//调用了_PyEval_EvalCodeWithName
return _PyEval_EvalCodeWithName(_co, globals, locals,
args, argcount,
kws, kws != NULL ? kws + 1 : NULL,
kwcount, 2,
defs, defcount,
kwdefs, closure,
NULL, NULL);
} PyObject *
_PyEval_EvalCodeWithName(PyObject *_co, PyObject *globals, PyObject *locals,
PyObject *const *args, Py_ssize_t argcount,
PyObject *const *kwnames, PyObject *const *kwargs,
Py_ssize_t kwcount, int kwstep,
PyObject *const *defs, Py_ssize_t defcount,
PyObject *kwdefs, PyObject *closure,
PyObject *name, PyObject *qualname)
{
...
//最终还是调用了PyEval_EvalFrameEx
retval = PyEval_EvalFrameEx(f,0);
...
return retval;
}

可以看到兜了这么多圈,最终PyRun_SimpleFileExFlags返回的值就是PyEval_EvalFrameEx返回的那个retval(当然出现异常,这里是NULL)。所以接下来会调用PyErr_Print,然后在PyErr_Print中,python虚拟机取出其维护的traceback对象,并遍历traceback链表,逐个输出其中的信息,也就是我们在python中看到的那个打印的异常信息。并且这个顺序是按照函数f、g、h,不是函数h、g、f。因为每一个栈帧对应一个traceback,而且是按照顺序遍历的,所以是函数f、g、h的顺序,当然从打印这一点也能看出来。

11.4.2 python中的异常捕获

目前我们知道了python中的异常在虚拟机级别是什么,抛出异常这个动作在虚拟机层面上是怎样的一个行为,最后我们还知道了python在处理异常时候的栈帧展开行为。但这只是python虚拟机中内建的处理异常的动作,并没有使用python语言中提供的异常捕获,下面我们就来看一下python提供了异常捕获机制是如何影响python虚拟机的异常处理流程的。

try:
raise Exception("raise an exception")
except Exception as e:
print(e)
finally:
print("finally code")
  1           0 SETUP_FINALLY           60 (to 62)
2 SETUP_EXCEPT 12 (to 16) 2 4 LOAD_NAME 0 (Exception)
6 LOAD_CONST 0 ('raise an exception')
8 CALL_FUNCTION 1
10 RAISE_VARARGS 1
12 POP_BLOCK
14 JUMP_FORWARD 42 (to 58) 3 >> 16 DUP_TOP
18 LOAD_NAME 0 (Exception)
20 COMPARE_OP 10 (exception match)
22 POP_JUMP_IF_FALSE 56
24 POP_TOP
26 STORE_NAME 1 (e)
28 POP_TOP
30 SETUP_FINALLY 14 (to 46) 4 32 LOAD_NAME 2 (print)
34 LOAD_NAME 1 (e)
36 CALL_FUNCTION 1
38 POP_TOP
40 POP_BLOCK
42 POP_EXCEPT
44 LOAD_CONST 1 (None)
>> 46 LOAD_CONST 1 (None)
48 STORE_NAME 1 (e)
50 DELETE_NAME 1 (e)
52 END_FINALLY
54 JUMP_FORWARD 2 (to 58)
>> 56 END_FINALLY
>> 58 POP_BLOCK
60 LOAD_CONST 1 (None) 6 >> 62 LOAD_NAME 2 (print)
64 LOAD_CONST 2 ('finally code')
66 CALL_FUNCTION 1
68 POP_TOP
70 END_FINALLY
72 LOAD_CONST 1 (None)
74 RETURN_VALUE

开头的SETUP_FINALLY、SETUP_EXCEPT和我们之间在for循环中介绍的SETUP_LOOP一样,都是调用了PyFrame_BlockSetup

//frameobject.c
void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
PyTryBlock *b;
//这个f_iblock为f_blockstack上的索引
//CO_MAXBLOCKS是一个宏,为20
if (f->f_iblock >= CO_MAXBLOCKS)
Py_FatalError("XXX block stack overflow");
//这个f_blockstack显然是个PyTryBlock结构体实例
b = &f->f_blockstack[f->f_iblock++];
b->b_type = type;
b->b_level = level;
b->b_handler = handler;
}
//frameobject.h
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;

事到如今,SETUP_FINALLYSETUP_EXCEPT两条指令不过是从f_blockstack中分走两块出去

《python解释器源码剖析》第11章--python虚拟机中的控制流

在这里分出两块PyTryBlock,肯定是要在捕捉异常的时候用。不过别着急,我们先回到抛出异常的地方看看:10 RAISE_VARARGS 1。在RAISE_VARARGS之前,通过LOAD_NAMELOAD_CONSTCALL_FUNCTION构造出了一个异常对象,当然尽管Exception是一个类,但调用同样是CALL_FUNCTION(至于这个指令的剖析和对象的创建后面章节会介绍,这里只需要知道一个异常已经被创建出来了),并将这个异常压入栈中。而RAISE_VARARGS指令的工作就从把这个异常对象从运行时栈取出开始。

        TARGET(RAISE_VARARGS) {
PyObject *cause = NULL, *exc = NULL;
switch (oparg) {
case 2:
cause = POP(); /* cause */
/* fall through */
case 1:
exc = POP(); /* exc */
/* fall through */
case 0:
if (do_raise(exc, cause)) {
why = WHY_EXCEPTION;
goto fast_block_end;
}
break;
default:
PyErr_SetString(PyExc_SystemError,
"bad RAISE_VARARGS oparg");
break;
}
goto error;
}

这里RAISE_VARARGS后面的参数是1,所以直接将异常对象取出赋给exc,然后调用do_raise函数。在do_raise中,最终调用之前的剖析过的PyErr_Restore函数,将异常对象存储到当前线程的状态对象中。在do_raise的最后,返回了一个WHY_EXCEPTION,这就是why的最终状态。在此之后,python虚拟机通过一个break的动作跳出了分发字节码指令的那个巨大的switch语句。一旦结束了字节码指令的分发,那么异常的捕获动作就有条不紊的展开了。在经过了一系列繁复的动作之后(比如创建并设置traceback对象),python虚拟机将携带着(why=WHY_EXCEPTION,f_iblock=2)的信息抵达真正捕捉异常的代码,我们看到跳转到了fast_block_end;

fast_block_end:
assert(why != WHY_NOT); /* 当出现异常时 */
while (why != WHY_NOT && f->f_iblock > 0) {
/* 获得SETUP_EXCEPT指令创建的PyTryBlock */
PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1]; ...
if (why == WHY_EXCEPTION && (b->b_type == SETUP_EXCEPT
|| b->b_type == SETUP_FINALLY)) {
PyObject *exc, *val, *tb;
...
//获得线程状态对象中的异常信息
PyErr_Fetch(&exc, &val, &tb);
PyErr_NormalizeException(
&exc, &val, &tb);
...
Py_INCREF(tb);
PUSH(tb);
PUSH(val);
PUSH(exc);
why = WHY_NOT;
JUMPTO(handler);
break;
}
if (b->b_type == SETUP_FINALLY) {
if (why & (WHY_RETURN | WHY_CONTINUE))
PUSH(retval);
PUSH(PyLong_FromLong((long)why));
why = WHY_NOT;
JUMPTO(b->b_handler);
break;
}
} /* unwind stack */

python虚拟机首先从当前的PyFrameObject对象中的f_blockstack中弹出一个PyTryBlock来,从代码中能看到弹出的是b_type = SETUP_EXCEPT, b_handler=16的PyTryBlock。另一方面,python虚拟机通过PyErr_Fetch得到了当前线程状态对象中存储的最新的异常对象和traceback对象:

//errors.c
void
PyErr_Fetch(PyObject **p_type, PyObject **p_value, PyObject **p_traceback)
{
PyThreadState *tstate = PyThreadState_GET(); *p_type = tstate->curexc_type;
*p_value = tstate->curexc_value;
*p_traceback = tstate->curexc_traceback; tstate->curexc_type = NULL;
tstate->curexc_value = NULL;
tstate->curexc_traceback = NULL;
}

回到fast_block_end,我们看到之后python虚拟机调用PUSH将tb、val、exc分别压入运行时栈中,并将why设置WHY_NOT。咦?为什么是WHY_NOT,这个我们知道是没有异常才会设置的呀,然而这里有异常。其实是因为python虚拟机发现了一个类型为SETUP_EXCEPT的PyTryBlock对象,已经意识到程序员在代码中已经为捕捉异常做好了准备,那么虚拟机任务自己的状态可以从WHY_EXCEPTION代表的发现异常状态切换为WHY_NOT代表的正常状态了。而接下来的异常处理工作,则需要交给程序员指定的代码来解决,这个动作通过JUMP_FORWARD(JUMPTO(b->b_handler))来完成。JUMPTO其实仅仅是进行了一下指令的跳跃,将python虚拟机将要执行的下一条指令设置为异常处理代码编译后所得到的第一条字节码指令。

SETUP_EXCEPT所创建的PyTryBlock中的b_handler为16,那么python虚拟机将要执行的下一条指令就是偏移量为16的那条指令,而这条指令就是DUP_POP,异常处理代码对应的第一条字节码指令。

首先我们except Exception,毫无疑问要LOAD_NAME,把这个异常给load进来,然后调用指令COMPARE_OP,这个显然就是比较我们指定捕获的异常和运行时栈中存在的那个被捕获的异常是否匹配。POP_JUMP_IF_FALSE如果为Py_True表示匹配,那么继续往下执行print(e)对应的字节码指令,POP_TOP将异常从栈顶弹出,赋值给e,然后打印等等。如果POP_JUMP_IF_FALSE为Py_False表示不匹配,那么我们发现直接跳转到了56 END_FINALLY,因为异常不匹配的话,那么异常的相关信息还是要重新放回线程当中,并重新设置why的状态,让python重新引发异常,而这个动作就由END_FINALLY完成,通过PyErr_Restore函数将异常信息重新协会线程状态中。

        PREDICTED(END_FINALLY);
TARGET(END_FINALLY) {
PyObject *status = POP();
if (PyLong_Check(status)) {
...
}
else if (PyExceptionClass_Check(status)) {
PyObject *exc = POP();
PyObject *tb = POP();
//通过这一步把异常信息又写回去了
PyErr_Restore(status, exc, tb);
why = WHY_EXCEPTION;
goto fast_block_end;
}
else if (status != Py_None) {
...
}
Py_DECREF(status);
DISPATCH();
}

然而不管异常是否匹配,最终处理异常的两条岔路都会在58 POP_BLOCK处汇合

        PREDICTED(POP_BLOCK);
TARGET(POP_BLOCK) {
PyTryBlock *b = PyFrame_BlockPop(f);
UNWIND_BLOCK(b);
DISPATCH();
}

这里将当前PyFrameObject的f_blockstack中还剩下的那个与SETUP_FINALLY对应的PyTryBlock对象弹出,然后python虚拟机的流程就进入了与finally表达式对应的字节码指令了。

因此在python的异常机制的实现中,最终要的就是why所表示的虚拟机状态及PyFrameObject对象中f_blockstack里存放的PyTryBlock对象了。变量why将指示python虚拟机当前是否发生了异常,而PyTryBlock对象则告诉python虚拟机,程序员是否为异常设置了except代码块和finally代码块,python虚拟机异常处理的流程就是在why和PyTryBlock的共同作用下完成的。

《python解释器源码剖析》第11章--python虚拟机中的控制流

11.5 思考题

e = 2.718
try:
raise Exception("我要引发异常了")
except Exception as e:
print(e) # 我要引发异常了 print(e)
# NameError: name 'e' is not defined

why?

我们发现在外面打印e的时候,告诉我们e没有被定义。这是为什么呢?首先可以肯定的是,肯定是except Exception as e导致的,因为我们as的也是e,和外面的e重名了,如果我们as的是e1呢?

e = 2.718
try:
raise Exception("我要引发异常了")
except Exception as e1:
print(e1) # 我要引发异常了 print(e) # 2.718

可以看到as的是e1就没有问题了,但是为什么呢?即便不知道原因,也能推测出来。因为外面的变量叫e,而我们捕获异常as的也是e,此时e的指向就变了,而当异常处理结束的时候,e这个变量就被销毁了。所以外面就找不到了,其实我相信即便不知道原因,也能推测出来。然而事实上也确实如此。我们可以看一下字节码,其实仔细观察我们上面例子的字节码,就已经能看出来了。

  1           0 LOAD_CONST               0 (2.718)
2 STORE_NAME 0 (e) 2 4 SETUP_FINALLY 12 (to 18) 3 6 LOAD_NAME 1 (Exception)
8 LOAD_CONST 1 ('我要引发异常了')
10 CALL_FUNCTION 1
12 RAISE_VARARGS 1
14 POP_BLOCK
16 JUMP_FORWARD 42 (to 60) 4 >> 18 DUP_TOP
20 LOAD_NAME 1 (Exception)
22 COMPARE_OP 10 (exception match)
24 POP_JUMP_IF_FALSE 58
26 POP_TOP
28 STORE_NAME 0 (e)
30 POP_TOP
32 SETUP_FINALLY 12 (to 46) 5 34 LOAD_NAME 2 (print)
36 LOAD_NAME 0 (e)
38 CALL_FUNCTION 1
40 POP_TOP
42 POP_BLOCK
44 BEGIN_FINALLY
>> 46 LOAD_CONST 2 (None)
48 STORE_NAME 0 (e)
50 DELETE_NAME 0 (e)
52 END_FINALLY
54 POP_EXCEPT
56 JUMP_FORWARD 2 (to 60)
>> 58 END_FINALLY
>> 60 LOAD_CONST 2 (None)
62 RETURN_VALUE

字节码很长,但是我们只需要看偏移量为50的那个字节码即可。你看到了什么,DELETE_NAME直接把e这个变量给删了,所以我们就找不到了,因此代码相当于下面这样:

e = 2.718
try:
raise Exception("我要引发异常了")
except Exception as e:
try:
print(e)
finally:
del e

因此在异常处理的时候,如果把异常赋予了一个变量,那么这个变量异常处理结束会被删掉,因此只能在except里面使用,这就是原因。但是原因有了,可动机呢?python这么做的动机是什么?根据官网文档解释:

当使用 as 将目标赋值为一个异常时,它将在 except 子句结束时被清除,这意味着异常必须赋值给一个不同的名称(不同于外部指定的变量),才能在 except 子句之后引用它(外部指定的变量)。异常会被清除是因为在附加了回溯信息的情况下,它们会形成堆栈帧的循环引用,使得所有局部变量保持存活直到发生下一次垃圾回收。

所以才要删除。

结束啦

《python解释器源码剖析》第11章--python虚拟机中的控制流

上一篇:《python解释器源码剖析》第4章--python中的list对象


下一篇:centos7在分区上建立文件系统和挂载