簡體   English   中英

Python中的異常處理是如何實現的?

[英]How is exception handling implemented in Python?

這個問題要求解釋如何以各種語言在后台實現異常處理,但它沒有收到 Python 的任何響應。

我對 Python 特別感興趣,因為 Python 以某種方式“鼓勵”通過EAFP 原則拋出和捕獲異常。

我從其他 SO 答案中了解到,如果預計很少會引發異常,那么 try/catch 塊比 if/else 語句便宜,並且調用深度很重要,因為填充堆棧跟蹤很昂貴。 這可能對所有編程語言都適用。

python 的特別之處在於 EAFP 原則的高優先級。 python 異常是如何在參考實現(CPython)內部實現的?

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)
{

我們有:

  • 一個SETUP_FINALLY指令,它可能將L1注冊為發生異常時跳轉到的位置(從技術上講,我猜它會將它推入堆棧,因為在我們的塊完成時必須恢復以前的值)。
  • S的代碼,即try:塊中的代碼。
  • 一條POP_BLOCK指令,用於清理內容(僅在 OK 的情況下達到;我猜如果出現異常,VM 會自動執行此操作)
  • 一個JUMP_FORWARD到 L0,這是下一條指令的位置(在try... except的塊除外)

這就是我們將在 OK 情況下運行的所有字節碼。 請注意,字節碼不需要主動檢查異常。 相反,虛擬機只會在出現異常的情況下自動跳轉到L1 這是在執行 RAISE_VARARGS 時在 ceval.c 中完成的。

那么在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的。 正如我之前寫的,它是執行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 */

有趣的部分是JUMPTO(handler)行。 handler值來自b->b_handler ,而它又由SETUP_FINALLY指令設置。 有了這個,我想我們已經繞了一圈! 哇!


as you probably know Python has a C API, and is written in C, so it is implemented in the C language (-> CPython). CPython 使用一些函數來檢查和處理異常,在此處記錄:

異常文檔

因此,這意味着異常本身也在 C 中實現(也在文檔中列出),這里有幾個例子:

PyExc_FileNotFoundError
PyExc_FileExistsError
PyExc_SyntaxError

ETC...

注意 -> 我不確定以下信息是否正確,但您可以在上述異常文檔中進行檢查。

我認為 CPython 檢查進程中的信號,如果信號被發送到給定進程 Python 檢查信號,這可能是一個例外。 不過我不確定。 不過,它確實會檢查信號!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM