- 大师兄的Python源码学习笔记(十八): 虚拟机中的控制流(五
- 大师兄的Python源码学习笔记(十六): 虚拟机中的控制流(三
- 大师兄的Python源码学习笔记(十七): 虚拟机中的控制流(四
- 大师兄的Python源码学习笔记(十五): 虚拟机中的控制流(二
- 大师兄的Python源码学习笔记(十九): 虚拟机中的函数机制(
- 大师兄的Python源码学习笔记(二十八): 虚拟机中的类机制(
- 大师兄的Python源码学习笔记(二十六): 虚拟机中的类机制(
- 大师兄的Python源码学习笔记(三十八): 模块的动态加载机制
- 大师兄的Python源码学习笔记(十二): Python虚拟机中
- 大师兄的Python源码学习笔记(二十一): 虚拟机中的函数机制
大师兄的Python源码学习笔记(十六): 虚拟机中的控制流(三)
大师兄的Python源码学习笔记(十八): 虚拟机中的控制流(五)
四、虚拟机中的异常控制流
1 虚拟机自身抛出异常
- Python内部有一套内建的异常捕捉机制,即使没有在代码中进行异常控制,Python脚本执行中所抛出的异常还是会被Python虚拟机捕捉到:
>>>a = 10
>>>b = 0
>>>c = a/b
Traceback (most recent call last):
File "demo.py", line 3, in <module>
c = a/b
ZeroDivisionError: division by zero
- 对应指令字节码:
1 0 LOAD_CONST 0 (10)
2 STORE_NAME 0 (a)
2 4 LOAD_CONST 1 (0)
6 STORE_NAME 1 (b)
3 8 LOAD_NAME 1 (a)
10 LOAD_NAME 0 (b)
12 BINARY_TRUE_DIVIDE
14 STORE_NAME 2 (c)
16 LOAD_CONST 2 (None)
18 RETURN_VALUE
- 这段字节码的核心部分是在BINARY_TRUE_DIVIDE,也就是除法计算:
ceval.c
TARGET(BINARY_TRUE_DIVIDE) {
PyObject *divisor = POP();
PyObject *dividend = TOP();
PyObject *quotient = PyNumber_TrueDivide(dividend, divisor);
Py_DECREF(dividend);
Py_DECREF(divisor);
SET_TOP(quotient);
if (quotient == NULL)
goto error;
DISPATCH();
}
- 在上面的代码中,PyNumber_TrueDivide(dividend, divisor)将栈中的两个值进行除法操作,并将结果赋值quotient ,如果quotient为NULL,则抛出异常。
- 深入研究PyNumber_TrueDivide,经过一系列的操作,除法路径最终会达到long_divrem,并抛出异常:
Objects\longobject.c
static int
long_divrem(PyLongObject *a, PyLongObject *b,
PyLongObject **pdiv, PyLongObject **prem)
{
Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b));
PyLongObject *z;
if (size_b == 0) {
PyErr_SetString(PyExc_ZeroDivisionError,
"integer division or modulo by zero");
return -1;
}
... ...
}
- 在上面的代码中,PyErr_SetString在抛出异常时,包含了一个异常对象PyExc_ZeroDivisionError。
- PyExc_ZeroDivisionError是一个PyObject对象异常指针,用来指明异常种类:
Include\pyerrors.h
...
PyAPI_DATA(PyObject *) PyExc_ReferenceError;
PyAPI_DATA(PyObject *) PyExc_SystemError;
PyAPI_DATA(PyObject *) PyExc_SystemExit;
PyAPI_DATA(PyObject *) PyExc_TypeError;
PyAPI_DATA(PyObject *) PyExc_UnboundLocalError;
PyAPI_DATA(PyObject *) PyExc_UnicodeError;
PyAPI_DATA(PyObject *) PyExc_UnicodeEncodeError;
PyAPI_DATA(PyObject *) PyExc_UnicodeDecodeError;
PyAPI_DATA(PyObject *) PyExc_UnicodeTranslateError;
PyAPI_DATA(PyObject *) PyExc_ValueError;
PyAPI_DATA(PyObject *) PyExc_ZeroDivisionError;
...
1.1在线程状态对象中记录异常信息
- 在抛出异常后,虚拟机会沿着PyErr_SetString一路向前,直到PyErr_Restore:
Python\errors.c
void
PyErr_SetString(PyObject *exception, const char *string)
{
PyObject *value = PyUnicode_FromString(string);
PyErr_SetObject(exception, value);
Py_XDECREF(value);
}
Python\errors.c
void
PyErr_SetObject(PyObject *exception, PyObject *value)
{
PyThreadState *tstate = PyThreadState_GET();
PyObject *exc_value;
PyObject *tb = NULL;
if (exception != NULL &&
!PyExceptionClass_Check(exception)) {
PyErr_Format(PyExc_SystemError,
"exception %R not a BaseException subclass",
exception);
return;
}
Py_XINCREF(value);
exc_value = _PyErr_GetTopmostException(tstate)->exc_value;
if (exc_value != NULL && exc_value != Py_None) {
/* Implicit exception chaining */
Py_INCREF(exc_value);
if (value == NULL || !PyExceptionInstance_Check(value)) {
/* We must normalize the value right now */
PyObject *fixed_value;
/* Issue #23571: functions must not be called with an
exception set */
PyErr_Clear();
fixed_value = _PyErr_CreateException(exception, value);
Py_XDECREF(value);
if (fixed_value == NULL) {
Py_DECREF(exc_value);
return;
}
value = fixed_value;
}
/* Avoid reference cycles through the context chain.
This is O(chain length) but context chains are
usually very short. Sensitive readers may try
to inline the call to PyException_GetContext. */
if (exc_value != value) {
PyObject *o = exc_value, *context;
while ((context = PyException_GetContext(o))) {
Py_DECREF(context);
if (context == value) {
PyException_SetContext(o, NULL);
break;
}
o = context;
}
PyException_SetContext(value, exc_value);
}
else {
Py_DECREF(exc_value);
}
}
if (value != NULL && PyExceptionInstance_Check(value))
tb = PyException_GetTraceback(value);
Py_XINCREF(exception);
PyErr_Restore(exception, value, tb);
}
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;
}
/* Save these in locals to safeguard against recursive
invocation through Py_XDECREF */
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);
}
- 在这里,虚拟机获取了当前的线程tstate,并将异常信息储存在当前线程,分别放在tstate->curexc_type、tstate->curexc_value和tstate->curexc_traceback中。
- 在Python中,我们可以通过sys库的接口获取当前线程的状态:
>>>import sys
>>>try:
>>> a = 10
>>> b = 0
>>> c = a/b
>>>except Exception:
>>> print(sys.exc_info()[0])
>>> print(sys.exc_info()[1])
<class 'ZeroDivisionError'>
division by zero
1.2 展开栈帧
- 跳出巨大的判断循环后,如果发生异常,将通过goto_error将why设置为WHY_EXCEPTION:
ceval.c
error:
assert(why == WHY_NOT);
why = WHY_EXCEPTION;
/* Double-check exception status. */
#ifdef NDEBUG
if (!PyErr_Occurred())
PyErr_SetString(PyExc_SystemError,
"error return without exception set");
#else
assert(PyErr_Occurred());
- 如果why为WHY_EXCEPTION,则表示执行字节码的过程中,有异常被抛出了。
ceval.c
/* Log traceback info. */
PyTraceBack_Here(f);
if (tstate->c_tracefunc != NULL)
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj,
tstate, f);
- 深入观察PyTraceBack_Here:
Python\traceback.c
int
PyTraceBack_Here(PyFrameObject *frame)
{
PyObject *exc, *val, *tb, *newtb;
PyErr_Fetch(&exc, &val, &tb);
newtb = tb_create_raw((PyTracebackObject *)tb, frame, frame->f_lasti,
PyFrame_GetLineNumber(frame));
if (newtb == NULL) {
_PyErr_ChainExceptions(exc, val, tb);
return -1;
}
PyErr_Restore(exc, val, newtb);
Py_XDECREF(tb);
return 0;
}
Python\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;
}
- 可以看到PyTraceBack_Here从当前线程中取出异常值,并创建了PyTracebackObject对象。
- 而PyTracebackObject对象是链表结构:
Include\traceback.h typedef struct _traceback { PyObject_HEAD struct _traceback *tb_next; struct _frame *tb_frame; int tb_lasti; int tb_lineno; } PyTracebackObject;
- 在创建对象后,虚拟机通过tb_create_raw将PyTracebackObject接入链表,并储存了当前最后执行的字节码指令以及其在源码中的行号。
Python\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);
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;
}
- 当虚拟机意识到有异常抛出,并创建了traceback对象后,会在当前栈帧中寻找except语句,如果没有找到,则退出当前活动栈帧,并沿着栈帧链表向上回退到上一个栈帧。
ceval.c
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");
- 如果异常没有被任何捕获动作捕获到,栈帧链表将一路回退,这个回退的过程被称为栈帧展开。
- 最终,虚拟机的执行流程将一直返回到PyRun_SimpleFileExFlags中:
Python\pythonrun.c
int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
PyCompilerFlags *flags)
{
...
if (v == NULL) {
Py_CLEAR(m);
PyErr_Print();
goto done;
}
...
}
- PyRun_SimpleFileExFlags最终通过PyErr_Print()从线程状态信息中取出traceback对象,遍历traceback对象链表,并逐个输出其中的信息。
Python\pythonrun.c
void
PyErr_PrintEx(int set_sys_last_vars)
{
PyObject *exception, *v, *tb, *hook;
if (PyErr_ExceptionMatches(PyExc_SystemExit)) {
handle_system_exit();
}
PyErr_Fetch(&exception, &v, &tb);
if (exception == NULL)
return;
PyErr_NormalizeException(&exception, &v, &tb);
if (tb == NULL) {
tb = Py_None;
Py_INCREF(tb);
}
PyException_SetTraceback(v, tb);
if (exception == NULL)
return;
/* Now we know v != NULL too */
if (set_sys_last_vars) {
if (_PySys_SetObjectId(&PyId_last_type, exception) < 0) {
PyErr_Clear();
}
if (_PySys_SetObjectId(&PyId_last_value, v) < 0) {
PyErr_Clear();
}
if (_PySys_SetObjectId(&PyId_last_traceback, tb) < 0) {
PyErr_Clear();
}
}
hook = _PySys_GetObjectId(&PyId_excepthook);
if (hook) {
PyObject* stack[3];
PyObject *result;
stack[0] = exception;
stack[1] = v;
stack[2] = tb;
result = _PyObject_FastCall(hook, stack, 3);
if (result == NULL) {
PyObject *exception2, *v2, *tb2;
if (PyErr_ExceptionMatches(PyExc_SystemExit)) {
handle_system_exit();
}
PyErr_Fetch(&exception2, &v2, &tb2);
PyErr_NormalizeException(&exception2, &v2, &tb2);
/* It should not be possible for exception2 or v2
to be NULL. However PyErr_Display() can't
tolerate NULLs, so just be safe. */
if (exception2 == NULL) {
exception2 = Py_None;
Py_INCREF(exception2);
}
if (v2 == NULL) {
v2 = Py_None;
Py_INCREF(v2);
}
fflush(stdout);
PySys_WriteStderr("Error in sys.excepthook:\n");
PyErr_Display(exception2, v2, tb2);
PySys_WriteStderr("\nOriginal exception was:\n");
PyErr_Display(exception, v, tb);
Py_DECREF(exception2);
Py_DECREF(v2);
Py_XDECREF(tb2);
}
Py_XDECREF(result);
} else {
PySys_WriteStderr("sys.excepthook is missing\n");
PyErr_Display(exception, v, tb);
}
Py_XDECREF(exception);
Py_XDECREF(v);
Py_XDECREF(tb);
}
网友评论