Python中的异常处理是如何实现的?

问题描述

This question 要求解释如何在各种语言的底层实现异常处理,但没有收到任何 Python 响应。

我对 Python 特别感兴趣,因为 Python 以某种方式“鼓励”通过 EAFP principle 抛出和捕获异常。

我从其他 SO 答案中了解到,如果预计很少引发异常,则 try/catch 块是 cheaper than an if/else statement,而 it's the call depth that's important 是因为填充堆栈跟踪的成本很高。这可能主要适用于所有编程语言。

python 的特别之处在于 EAFP 原则的高优先级。因此,python 异常如何在参考实现 (cpython) 中内部实现?

解决方法

try ... exceptthe compiler 中有一些不错的文档:

/*
   Code generated for "try: S except E1 as V1: S1 except E2 as V2: S2 ...":
   (The contents of the value stack is shown in [],with the top
   at the right; 'tb' is trace-back info,'val' the exception's
   associated value,and 'exc' the exception.)
   Value stack          Label   Instruction     Argument
   []                           SETUP_FINALLY   L1
   []                           <code for S>
   []                           POP_BLOCK
   []                           JUMP_FORWARD    L0
   [tb,val,exc]       L1:     DUP                             )
   [tb,exc,exc]          <evaluate E1>                   )
   [tb,E1]      JUMP_IF_NOT_EXC_MATCH L2        ) only if E1
   [tb,exc]               POP
   [tb,val]                    <assign to V1>  (or POP if no V1)
   [tb]                         POP
   []                           <code for S1>
                                JUMP_FORWARD    L0
   [tb,exc]       L2:     DUP
   .............................etc.......................
   [tb,exc]       Ln+1:   RERAISE     # re-raise exception
   []                   L0:     <next statement>
   Of course,parts are not generated if Vi or Ei is not present.
*/
static int
compiler_try_except(struct compiler *c,stmt_ty s)
{

我们有:

  • 一条 SETUP_FINALLY 指令,大概将 L1 注册为发生异常时跳转到的位置(从技术上讲,我猜它会将其压入堆栈,因为必须恢复先前的值当我们的块完成时)。
  • S 的代码,即 try: 块内的代码。
  • 一条 POP_BLOCK 指令,用于清理内容(仅在 OK 情况下才能达到;我猜如果出现异常,VM 会自动执行)
  • 一个 JUMP_FORWARD 到 L0,它是下一条指令的位置(在 try ... except 块之外)

这就是我们将在 OK 情况下运行的所有字节码。请注意,字节码不需要主动检查异常。相反,虚拟机会在出现异常时自动跳转到 L1。这是在 ceval.c when executing RAISE_VARARGS 中完成的。

那么在 L1 会发生什么?简单地说,我们按顺序检查每个 except 子句:它是否与当前引发的异常匹配?如果是,我们运行该 except 块中的代码并跳转到 L0try ... except 块外的第一条指令)。如果不是,我们检查下一个 except 子句,或者如果没有子句匹配,则重新引发异常。

但让我们更具体一点。 dis 模块让我们转储字节码。所以让我们创建两个小的 python 文件。

一个检查:

tmp$ cat if.py
if type(x) is int:
    x += 1
else:
    print('uh-oh')

...还有一个捕获:

tmp$ cat try.py
try:
    x += 1
except TypeError as e:
    print('uh-oh')

现在,让我们转储他们的字节码:

tmp$ python3 -m dis if.py
  1           0 LOAD_NAME                0 (type)
              2 LOAD_NAME                1 (x)
              4 CALL_FUNCTION            1
              6 LOAD_NAME                2 (int)
              8 COMPARE_OP               8 (is)
             10 POP_JUMP_IF_FALSE       22

  2          12 LOAD_NAME                1 (x)
             14 LOAD_CONST               0 (1)
             16 INPLACE_ADD
             18 STORE_NAME               1 (x)
             20 JUMP_FORWARD             8 (to 30)

  4     >>   22 LOAD_NAME                3 (print)
             24 LOAD_CONST               1 ('uh-oh')
             26 CALL_FUNCTION            1
             28 POP_TOP
        >>   30 LOAD_CONST               2 (None)
             32 RETURN_VALUE

对于成功的案例,这将运行 13 条指令(从 0 到 20,然后是 30 和 32)。

tmp$ python3 -m dis try.py 
  1           0 SETUP_EXCEPT            12 (to 14)

  2           2 LOAD_NAME                0 (x)
              4 LOAD_CONST               0 (1)
              6 INPLACE_ADD
              8 STORE_NAME               0 (x)
             10 POP_BLOCK
             12 JUMP_FORWARD            42 (to 56)

  3     >>   14 DUP_TOP
             16 LOAD_NAME                1 (TypeError)
             18 COMPARE_OP              10 (exception match)
             20 POP_JUMP_IF_FALSE       54
             22 POP_TOP
             24 STORE_NAME               2 (e)
             26 POP_TOP
             28 SETUP_FINALLY           14 (to 44)

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

对于成功的案例,这将运行 9 条指令(包括 0-12,然​​后是 56 和 58)。

现在,指令计数远不是所用时间的完美衡量标准(尤其是在字节码虚拟机中,指令的成本可能变化很大),但确实如此。

最后,让我们看看 CPython 如何“自动”跳转到 L1。正如我之前写的,它作为 the execution of RAISE_VARARGS 的一部分发生:

    case 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(tstate,cause)) {
                goto exception_unwind;
            }
            break;
        default:
            _PyErr_SetString(tstate,PyExc_SystemError,"bad RAISE_VARARGS oparg");
            break;
        }
        goto error;
    }

[...]

exception_unwind:
    f->f_state = FRAME_UNWINDING;
    /* 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) {
            UNWIND_EXCEPT_HANDLER(b);
            continue;
        }
        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,f->f_lasti,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,&tb);
            if (tb != NULL)
                PyException_SetTraceback(val,tb);
            else
                PyException_SetTraceback(val,Py_None);
            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);
            PUSH(tb);
            PUSH(val);
            PUSH(exc);
            JUMPTO(handler);
            if (_Py_TracingPossible(ceval2)) {
                trace_info.instr_prev = INT_MAX;
            }
            /* Resume normal execution */
            f->f_state = FRAME_EXECUTING;
            goto main_loop;
        }
    } /* unwind stack */

有趣的部分是 JUMPTO(handler) 行。 handler 值来自 b->b_handler,而 SETUP_FINALLY 又由 {{1}} 指令设置。有了这个,我想我们已经回到了原点!哇!

,
您可能知道 Python 有一个 C API,并且是用 C 编写的,因此它是用 C 语言 (-> CPython) 实现的。 CPython 使用一些函数来检查和处理异常,这里记录了它:

Exception Docs .

这意味着异常本身也在 C 中实现(也在文档中列出),这里有几个例子:

PyExc_FileNotFoundError
PyExc_FileExistsError
PyExc_SyntaxError

等等...

注意 -> 我不确定以下信息是否正确,但您可以在上述异常文档中进行事实检查。

我认为 CPython 会检查进程中的信号,如果信号被发送到给定进程 Python 会检查信号,这可能是一个例外。我不确定。不过,它确实会检查信号!