简体   繁体   English

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

[英]How is exception handling implemented in Python?

This question asked for explanations of how exception handling is implemented under the hood in various languages but it did not receive any responses for Python. 这个问题要求解释如何以各种语言在后台实现异常处理,但它没有收到 Python 的任何响应。

I'm especially interested in Python because Python somehow "encourages" exception throwing and catching via the EAFP principle .我对 Python 特别感兴趣,因为 Python 以某种方式“鼓励”通过EAFP 原则抛出和捕获异常。

I've learned from other SO answers that a try/catch block is cheaper than an if/else statement if the exception is expected to be raised rarely, and that it's the call depth that's important because filling the stacktrace is expensive.我从其他 SO 答案中了解到,如果预计很少会引发异常,那么 try/catch 块比 if/else 语句便宜,并且调用深度很重要,因为填充堆栈跟踪很昂贵。 This is probably principally true for all programming languages.这可能对所有编程语言都适用。

What's special about python though is the high priority of the EAFP principle. python 的特别之处在于 EAFP 原则的高优先级。 How are python exceptions therefore implemented internally in the reference implementation (CPython)? python 异常是如何在参考实现(CPython)内部实现的?

try... except has some nice documentation in the compiler : try... except编译器中有一些不错的文档:

/*
   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, val, exc, exc]          <evaluate E1>                   )
   [tb, val, exc, exc, E1]      JUMP_IF_NOT_EXC_MATCH L2        ) only if E1
   [tb, val, exc]               POP
   [tb, val]                    <assign to V1>  (or POP if no V1)
   [tb]                         POP
   []                           <code for S1>
                                JUMP_FORWARD    L0
   [tb, val, exc]       L2:     DUP
   .............................etc.......................
   [tb, val, 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)
{

We have:我们有:

  • a SETUP_FINALLY instruction, which presumably registers L1 as the location to jump to when an exception occurs (technically, I'd guess it pushes it on a stack, since the previous value must be restored when our block is done).一个SETUP_FINALLY指令,它可能将L1注册为发生异常时跳转到的位置(从技术上讲,我猜它会将它推入堆栈,因为在我们的块完成时必须恢复以前的值)。
  • the code for S , that is, the code inside the try: block. S的代码,即try:块中的代码。
  • a POP_BLOCK instruction, which cleans stuff up (only reached in the OK case; I'm guessing the VM does it automatically if there's an exception)一条POP_BLOCK指令,用于清理内容(仅在 OK 的情况下达到;我猜如果出现异常,VM 会自动执行此操作)
  • a JUMP_FORWARD to L0, which is the location of the next instruction (outside the try... except blocks)一个JUMP_FORWARD到 L0,这是下一条指令的位置(在try... except的块除外)

And that's all the bytecode we will run in the OK case.这就是我们将在 OK 情况下运行的所有字节码。 Note that the bytecode doesn't need to actively check for exceptions.请注意,字节码不需要主动检查异常。 Instead, the virtual machine will just automatically jump to L1 in the case of an exception.相反,虚拟机只会在出现异常的情况下自动跳转到L1 This is done in ceval.c when executing RAISE_VARARGS .这是在执行 RAISE_VARARGS 时在 ceval.c 中完成的。

So what happens at L1 ?那么在L1会发生什么? Simply put, we check each except clause in order: does it match the currently raised exception?简单地说,我们按顺序检查每个except子句:它是否匹配当前引发的异常? If it does, we run the code in that except block and jump to L0 (the first instruction outside the try... except blocks).如果是这样,我们在那个except块中运行代码并跳转到L0try... except块之外的第一条指令)。 If not, we check the next except clause, or re-raise the exception if no clause matched.如果不是,我们检查下一个except子句,如果没有子句匹配,则重新引发异常。

But let's be more concrete about it.但让我们更具体一点。 The dis module lets us dump bytecode. dis模块让我们转储字节码。 So let's create two tiny python files.因此,让我们创建两个微小的 python 文件。

One that checks:一个检查:

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

...and one that catches: ......还有一个抓住了:

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

Now, let's dump their bytecode:现在,让我们转储他们的字节码:

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

For the successful case, this will run 13 instructions (from 0-20 inclusive, then 30 and 32).对于成功的案例,这将运行 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

For the successful case, this will run 9 instructions (0-12 inclusive, then 56 and 58).对于成功的案例,这将运行 9 条指令(包括 0-12,然后是 56 和 58)。

Now, instruction count is far from a perfect measure of time taken (especially in a bytecode vm, where instructions can vary wildly in cost), but there it is.现在,指令计数远不是一个完美的时间衡量标准(尤其是在字节码虚拟机中,指令的成本可能会有很大差异),但确实如此。

Finally, let's look at how CPython does that "automatic" jump to L1 .最后,让我们看看 CPython 是如何“自动”跳转到L1的。 As I wrote earlier, it happens as part of the execution of RAISE_VARARGS :正如我之前写的,它是执行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, exc, 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, &exc, &val, &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 */

The interesting part is the JUMPTO(handler) line.有趣的部分是JUMPTO(handler)行。 The handler value comes from b->b_handler , which in turn was set by the SETUP_FINALLY instruction. handler值来自b->b_handler ,而它又由SETUP_FINALLY指令设置。 And with that, I think we've come full circle!有了这个,我想我们已经绕了一圈! Whew!哇!


as you probably know Python has a C API, and is written in C, so it is implemented in the C language (-> CPython). as you probably know Python has a C API, and is written in C, so it is implemented in the C language (-> CPython). There are a few functions that CPython uses to check and handle exceptions, it is documented here: CPython 使用一些函数来检查和处理异常,在此处记录:

Exception Docs . 异常文档

Thus meaning the exceptions themselfes are also implemented in C (also listed in the docs), here are a few examples:因此,这意味着异常本身也在 C 中实现(也在文档中列出),这里有几个例子:

PyExc_FileNotFoundError PyExc_FileNotFoundError
PyExc_FileExistsError PyExc_FileExistsError
PyExc_SyntaxError PyExc_SyntaxError

etc... ETC...

Note -> I am not sure if the following information is correct or not, but you'll be able to fact check it in the said exception documentations.注意 -> 我不确定以下信息是否正确,但您可以在上述异常文档中进行检查。

I think CPython checks for signals in the process, if a signal got sent to the given process Python checks the signal, which could be an exception.我认为 CPython 检查进程中的信号,如果信号被发送到给定进程 Python 检查信号,这可能是一个例外。 I am not sure though.不过我不确定。 It defenetly checks for signals though!不过,它确实会检查信号!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM