Python虚拟机之异常控制流(五)

时间:2022-10-20 21:06:16

Python中的异常控制语义结构

Python虚拟机之异常控制流(四)这一章中,我们考察了Python的异常在虚拟机中的级别上是什么东西,抛出异常这个动作在虚拟机的级别上对应的行为,最后,我们还剖析了Python在处理异常时栈帧展开行为。但遗憾的是,在前面,我们只考察了Python虚拟机中内建的处理异常的动作,并没有使用Python语言提供的异常控制结构。本章我们将研究Python语言提供的异常控制结构如何影响Python虚拟机的异常处理流程

# cat demo6.py
try:
raise Exception("I am an exception")
except Exception, e:
print(e)
finally:
print("The finally code")
# python2.5
……
>>> source = open("demo6.py").read()
>>> co = compile(source, "demo6.py", "exec")
>>> import dis
>>> dis.dis(co)
1 0 SETUP_FINALLY 49 (to 52)
3 SETUP_EXCEPT 16 (to 22) 2 6 LOAD_NAME 0 (Exception)
9 LOAD_CONST 0 ('I am an exception')
12 CALL_FUNCTION 1
15 RAISE_VARARGS 1
18 POP_BLOCK
19 JUMP_FORWARD 26 (to 48) 3 >> 22 DUP_TOP
23 LOAD_NAME 0 (Exception)
26 COMPARE_OP 10 (exception match)
29 JUMP_IF_FALSE 14 (to 46)
32 POP_TOP
33 POP_TOP
34 STORE_NAME 1 (e)
37 POP_TOP 4 38 LOAD_NAME 1 (e)
41 PRINT_ITEM
42 PRINT_NEWLINE
43 JUMP_FORWARD 2 (to 48)
>> 46 POP_TOP
47 END_FINALLY
>> 48 POP_BLOCK
49 LOAD_CONST 1 (None) 6 >> 52 LOAD_CONST 2 ('The finally code')
55 PRINT_ITEM
56 PRINT_NEWLINE
57 END_FINALLY
58 LOAD_CONST 1 (None)
61 RETURN_VALUE

  

开始的两条字节码指令似曾相识,其实前面剖析Python循环结构的时候,我们曾说过,SETUP_FINALLY和SETUP_EXCEPT一样,它们都是调用PyFrame_BlockSetup,我们再来看一下PyFrame_BlockSetup的代码

frameobject.c

void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
PyTryBlock *b;
if (f->f_iblock >= CO_MAXBLOCKS)
Py_FatalError("XXX block stack overflow");
b = &f->f_blockstack[f->f_iblock++];
b->b_type = type;
b->b_level = level;
b->b_handler = handler;
}

  

frameobject.h

typedef struct _frame {
……
int f_iblock; /* index in f_blockstack */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
……
} PyFrameObject;

  

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_FINALLY和SETUP_EXCEPT两条指令不过是从f_blockstack中分两块走出,如图1-1

Python虚拟机之异常控制流(五)

图1-1   SETUP_FINALLY和SETUP_EXCEPT完成后的的f_blockstack

在这里分出两块,肯定是要在捕捉异常时使用。不过不用心急,让我们先回到抛出异常的地方"15   RAISE_VARARGS   1"。在RAISE_VARARGS指令之前,通过"LOAD_NAME   0"、"LOAD_CONST   0"、"CALL_FUNCTION   1"构造出一个异常对象,并将此异常对象压入运行时栈中,而RAISE_VARARGS指令的工作就是把这个异常对象从运行时栈中取出

ceval.c

case RAISE_VARARGS:
u = v = w = NULL;
switch (oparg)
{
case 3:
u = POP(); /* traceback */
/* Fallthrough */
case 2:
v = POP(); /* value */
/* Fallthrough */
case 1:
w = POP(); /* exc */
case 0: /* Fallthrough */
why = do_raise(w, v, u);
break;
default:
PyErr_SetString(PyExc_SystemError,
"bad RAISE_VARARGS oparg");
why = WHY_EXCEPTION;
break;
}
break;

  

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

ceval.c

PyObject * PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
……
for (;;)
{
……
//巨大的switch语句
switch (opcode)
{
……
}
……
while (why != WHY_NOT && f->f_iblock > 0)
{
//[1]:获得SETUP_EXCEPT指令创建的PyTryBlock
PyTryBlock *b = PyFrame_BlockPop(f);
……
if (b->b_type == SETUP_FINALLY ||
(b->b_type == SETUP_EXCEPT &&
why == WHY_EXCEPTION))
{
if (why == WHY_EXCEPTION)
{
PyObject *exc, *val, *tb;
//[2]:获得线程状态对象中的异常信息
PyErr_Fetch(&exc, &val, &tb);
……
if (tb == NULL)
{
……
}
else
PUSH(tb);
PUSH(val);
PUSH(exc);
}
else
{
……
}
why = WHY_NOT;
JUMPTO(b->b_handler);
break;
}
}
……
//尝试捕捉异常
if (why != WHY_NOT)//[3]:不存在异常处理代码,展开堆栈
break;
……
}
……
}

  

在上面的代码的[1]处,Python虚拟机首先从当前的PyFrameObject对象中的f_blockstack中弹出一个PyTryBlock来,在图1-1可以看到,弹出的这个是b_type=SETUP_EXCEPT、b_handler=22的PyTryBlock。另一方面,在代码的[2]处,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;
}

  

随后,Python虚拟机将tb、val、exc分别压入运行时栈中,并将why设置为WHY_NOT。这又引出一个问题,为什么是WHY_NOT?不是已经发生异常了吗?没错,确实应该是WHY_NOT,不要忘了,why被设置为WHY_NOT是发生在if (b->b_type == SETUP_FINALLY || (b->b_type == SETUP_EXCEPT &&why == WHY_EXCEPTION))这个语句里,Python虚拟机意识到程序员要捕捉这个异常,所以将WHY原先的状态WHY_EXCEPTION转为WHY_NOT,代表程序从异常状态转为正常状态

而接下来的异常处理工作,则需要交给程序员指定的代码来处理,这个动作通过JUMPTO(b->b_handler)来完成。JUMPTO其实仅仅是进行了一下指令的跳跃,将Python虚拟机将要执行的下一条指令设置为异常处理代码编译后所得到的第一条字节码指令

在demo6.py中,SETUP_EXCEPT所创建的PyTryBlock中的b_handler为22,这时Python虚拟机将要执行的下一条指令就是偏移量为22的那条指令,从demo6.py的编译结果来看,正好就是"22   DUP_TOP",异常处理代码的第一条字节码指令

在处理异常的字节码指令时,有一条"26   COMPARE_OP   10"指令,这条指令将比较运行时栈中的那个被捕捉到的异常是否跟except表达式中指定的异常匹配。随后通过一条进行指令跳跃的字节码指令"29   JUMP_IF_FALSE   14   (to 46)"来判断是否需要进行指令跳跃。如果COMPARE_OP的操作结果发现异常匹配,那么JUMP_IF_FALSE就不会进行指令跳跃,而是接着执行"print(e)"表达式对应的字节码指令。而如果COMPARE_OP发现异常不匹配,那么JUMP_IF_FALSE将跳跃到偏移量为46的字节码指令:POP_TOP,那么就会忽略原先"print(e)"表达式对应的字节码指令

当异常不匹配时,会比异常匹配时多执行两条字节码指令"46   POP_TOP"和"47   END_FINALLY "。前面提到,在进入except表达式之前,Python虚拟机从当前线程的状态对象中将当前的异常信息取出,分别为tb、val和exc,并将其分别压入运行时栈中。当异常匹配时,它们都会从栈中取出并处理掉,如果异常不匹配,就是说虽然有except表达式,但实际上并没有处理异常的代码。那么已经取出的异常信息显然不能扔掉,而要重新放回线程状态对象中,然后重新设置why的状态,让Python虚拟机的内部状态重新进入到“异常发生”这种状态,并开始栈帧展开的动作,寻找真正能处理异常的代码。所以这两条指令就是完成这个“重返异常状态”的使命,从END_FINALLY指令的实现代码可以清晰看到这一点

ceval.c

case END_FINALLY:
v = POP();
if (PyInt_Check(v))
{
why = (enum why_code)PyInt_AS_LONG(v);
assert(why != WHY_YIELD);
if (why == WHY_RETURN ||
why == WHY_CONTINUE)
retval = POP();
}
else if (PyExceptionClass_Check(v) || PyString_Check(v))
{
w = POP();
u = POP();
PyErr_Restore(v, w, u);
why = WHY_RERAISE;
break;
}
else if (v != Py_None)
{
PyErr_SetString(PyExc_SystemError,
"'finally' pops bad exception");
why = WHY_EXCEPTION;
}
Py_DECREF(v);
break;

  

Python虚拟机通过PyErr_Restore重新设置了异常信息,并把why的状态设置为WHY_RERAISE了。

不管异常是否匹配,最终处理异常的两条岔路都会在"48   POP_BLOCK"会合

ceval.c

case POP_BLOCK:
{
PyTryBlock *b = PyFrame_BlockPop(f);
while (STACK_LEVEL() > b->b_level)
{
v = POP();
Py_DECREF(v);
}
}
continue;

  

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

这里我们总结一下,Python的异常机制的实现中,最重要的就是why所表示的虚拟机状态及PyFrameObject对象中f_blockstack里存放的PyTryBlock对象了。变量why将指示Python虚拟机当前是否发生了异常,而PyTryBlock对象则指示了程序员是否为异常设置了except代码块和finally代码块。Python虚拟机处理异常的过程就是在why和PyTryBlock的共同作用下完成的。在图1-2中给出了Python中实现异常机制的详细流程:

Python虚拟机之异常控制流(五)

图1-2   Python中异常机制的流程图