Skip to content

Latest commit

 

History

History
377 lines (312 loc) · 12.5 KB

File metadata and controls

377 lines (312 loc) · 12.5 KB

异常处理

当代码中发生异常而又没有被当前的 frame 捕获时, Python 会将异常一直往上传递, 直到被某个 frame 中的代码捕获, 如果最终仍然没有一个 frame 捕获异常, 那么 Python 的入口点(不同的启动方式有不同的入口点, 例 如执行文件, 交互式 shell等等) 会打印出异常信息并退出.

异常对象形成一个单向链表, 越靠近头部的节点越处于调用链(call frame)的上层, 越靠近尾部的节点越接近异常发生的地方.

Python 如何处理异常

测试的 Python 代码:

try:
    x = 1 / 0
except ZeroDivisionError as e:
    print(e)

使用 dis 查看对应的字节码:

  1           0 SETUP_FINALLY           12 (to 14)

  2           2 LOAD_CONST               0 (1)
              4 LOAD_CONST               1 (0)
              6 BINARY_TRUE_DIVIDE
              8 STORE_NAME               0 (x)
             10 POP_BLOCK
             12 JUMP_FORWARD            44 (to 58)

  3     >>   14 DUP_TOP
             16 LOAD_NAME                1 (ZeroDivisionError)
             18 JUMP_IF_NOT_EXC_MATCH    56
             20 POP_TOP
             22 STORE_NAME               2 (e)
             24 POP_TOP
             26 SETUP_FINALLY           20 (to 48)

  4          28 LOAD_NAME                3 (print)
             30 LOAD_NAME                2 (e)
             32 CALL_FUNCTION            1
             34 POP_TOP
             36 POP_BLOCK
             38 POP_EXCEPT
             40 LOAD_CONST               2 (None)
             42 STORE_NAME               2 (e)
             44 DELETE_NAME              2 (e)
             46 JUMP_FORWARD            10 (to 58)
        >>   48 LOAD_CONST               2 (None)
             50 STORE_NAME               2 (e)
             52 DELETE_NAME              2 (e)
             54 RERAISE
        >>   56 RERAISE
        >>   58 LOAD_CONST               2 (None)
             60 RETURN_VALUE

和异常处理相关的字节码有:

  • SETUP_FINALLY

  • JUMP_IF_NOT_EXC_MATCH

  • RERAISE

  • POP_EXCEPT

SETUP_FINALLY

SETUP_FINALLY 对应的处理器:

case TARGET(SETUP_FINALLY): {
    PyFrame_BlockSetup(f, SETUP_FINALLY, INSTR_OFFSET() + oparg,
                        STACK_LEVEL());
    DISPATCH();
}
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;
}
typedef struct {
    int b_type;                 /* what kind of block this is */
    int b_handler;              /* where to jump to find handler, b_handler 其实就是跳转地址 */
    int b_level;                /* value stack level to pop to, b_level 保存当前栈顶的位置 */
} PyTryBlock;

PyTryBlock 保存了异常处理的重要信息, 当异常发生时可以从 PyTryBlock 得知异常处理的代码的位置.

异常发生时: 除 0 错误

BINARY_TRUE_DIVIDE 的处理器如下:

case 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 最终会调用整数对象的除法函数:

static PyObject *
long_true_divide(PyObject *v, PyObject *w)
{
    if (b_size == 0) {
        PyErr_SetString(PyExc_ZeroDivisionError,
                        "division by zero");
        goto error;
    }
  error:
    return NULL;
}

PyErr_SetString 会设置 tstate->curexc_type, tstate->curexc_valuetstate->curexc_traceback. 这三个变量记录了当前异常的信息.

PyThreadState 中保存着异常相关的信息:

struct _ts {
    /* The exception currently being raised */
    PyObject *curexc_type;
    PyObject *curexc_value;
    PyObject *curexc_traceback;
}
  • curexc_type, 异常类型.

  • curexc_value, 异常信息.

  • curexc_traceback, PyTracebackObject 对象.

执行流程最终回到 ceval, quotient 等于 NULL, 跳转到处理异常的代码.

ceval 处理异常

/* Log traceback info. */
PyTraceBack_Here(f);

int
PyTraceBack_Here(PyFrameObject *frame)
{
    PyObject *exc, *val, *tb, *newtb;
    PyErr_Fetch(&exc, &val, &tb);
    newtb = _PyTraceBack_FromFrame(tb, frame);
    if (newtb == NULL) {
        _PyErr_ChainExceptions(exc, val, tb);
        return -1;
    }
    PyErr_Restore(exc, val, newtb);
    Py_XDECREF(tb);
    return 0;
}

PyTraceBack_Here 生成新的 PyTracebackObject 对象.

typedef struct _traceback {
    PyObject_HEAD
    struct _traceback *tb_next;
    struct _frame *tb_frame;
    int tb_lasti;
    int tb_lineno;
} PyTracebackObject;


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;
}

PyTracebackObject 对象记录了异常发生时的信息, 例如 frame, 发生异常的代码所在的行号和字节码地址. 这些信息也就是当 Python 解释器因为异常退出时在终端打印的异常栈信息.

跳转到异常处理代码

exception_unwind:
        /* Unwind stacks if an exception occurred */
        while (f->f_iblock > 0) {
            /* Pop the current block. */
            PyTryBlock *b = &f->f_blockstack[--f->f_iblock];

            if (b->b_type == EXCEPT_HANDLER) {
                // 取出栈上的异常信息
                // exc_info->exc_type = POP(); 
                // exc_info->exc_value = POP(); 
                // exc_info->exc_traceback = POP();
                UNWIND_EXCEPT_HANDLER(b);
                continue;
            }
            // 清空栈中在 b->b_level 上面的元素
            // 例如, 如果 b->b_type 等于 SETUP_FINALLY, 那么清空自 SETUP_FINALLY 指令之后栈上新增的元素.
            UNWIND_BLOCK(b);
            if (b->b_type == SETUP_FINALLY) {
                PyObject *exc, *val, *tb;
                int handler = b->b_handler;
                _PyErr_StackItem *exc_info = tstate->exc_info;
                /* Beware, this invalidates all b->b_* fields */
                PyFrame_BlockSetup(f, EXCEPT_HANDLER, -1, STACK_LEVEL());
                // 保存上一个异常信息
                PUSH(exc_info->exc_traceback);
                PUSH(exc_info->exc_value);
                if (exc_info->exc_type != NULL) {
                    PUSH(exc_info->exc_type);
                }
                else {
                    Py_INCREF(Py_None);
                    PUSH(Py_None);
                }
                _PyErr_Fetch(tstate, &exc, &val, &tb);
                /* Make the raw exception data
                   available to the handler,
                   so a program can emulate the
                   Python main loop. */
                _PyErr_NormalizeException(tstate, &exc, &val, &tb);
                // 设置异常的 traceback 属性.
                if (tb != NULL)
                    PyException_SetTraceback(val, tb);
                else
                    PyException_SetTraceback(val, Py_None);
                // 保存当前异常信息到 tstate->exc_info
                Py_INCREF(exc);
                exc_info->exc_type = exc;
                Py_INCREF(val);
                exc_info->exc_value = val;
                exc_info->exc_traceback = tb;
                if (tb == NULL)
                    tb = Py_None;
                Py_INCREF(tb);
                // 将异常信息放入栈上, except 代码块需要使用这些异常信息和 except 语句中的异常进行对比.
                PUSH(tb);
                PUSH(val);
                PUSH(exc);
                JUMPTO(handler);
                if (_Py_TracingPossible(ceval2)) {
                    int needs_new_execution_window = (f->f_lasti < instr_lb || f->f_lasti >= instr_ub);
                    int needs_line_update = (f->f_lasti == instr_lb || f->f_lasti < instr_prev);
                    /* Make sure that we trace line after exception if we are in a new execution
                     * window or we don't need a line update and we are not in the first instruction
                     * of the line. */
                    if (needs_new_execution_window || (!needs_line_update && instr_lb > 0)) {
                        instr_prev = INT_MAX;
                    }
                }
                /* Resume normal execution */
                goto main_loop;
            }
        } /* unwind stack */

RERAISE

如果没有 except 可以捕获异常, 那么最后该异常会被重新触发. RERAISE 的作用就是重新触发异常.

如果处理异常时发生了别的异常

例如, 如果在处理异常 A 时, 有发生了异常 B, 那么 Python 内部会记录下这种嵌套异常的关系. 异常对象中有一个 context 属性, 通过该 context 便可以知道是否发生了嵌套的异常.

/* exception 是异常类, value 一般是一个字符串, 表示错误信息.
    主要作用是调用 _PyErr_Restore(tstate, exception, value, tb) 记录下该异常信息.
    
    如果该异常是嵌套异常, 那么设置该异常的 context 属性, PyException_SetContext(value, exc_value).
 */
void
_PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
{
    PyObject *exc_value;
    PyObject *tb = NULL;
    
    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(tstate);

            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. */
        
        /* 异常对象的 context 等于上一个异常对象, 
            例如在处理异常 A 的 except 代码块中又发生了异常 B, 那么异常 B 的 context 就是异常 A
        */
        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); // 设置 context
        }
        else {
            Py_DECREF(exc_value);
        }
    }
}

总结

之前一直不清楚 Python 内部是如何实现异常处理的, 通过阅读源码终于弄懂了内部的实现原理.

当 ceval 执行字节码遇到错误时, 会调用 PyErr_SetString(PyObject *exception, const char *string) 设置异常信息, 异常信息保存在线程对象中, 具体是 tstate->curexc_type, tstate->curexc_valuetstate->curexc_traceback, 然后跳出 switch case 来到错误处理的代码.

如果错误发生在 try/except 中, 那么通过 PyTryBlock 可以知道处理异常的代码的地址, 将异常信息(tstate->curexc_*) 保存到 tstate->exc_info, 以及栈上, 然后重置 tstate->curexc_*, 最后跳转到处理异常的代码. 如果没有匹配的 except, 那么异常会被重新触发, 重复异常处理的流程.

如果在当前栈帧没有捕获异常, 那么异常会上溯到上一层栈帧, 直到有一个栈帧捕获异常或退出程序.

如果错误没有被捕获, 那么最终会终止程序, 打印异常栈信息.