午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

虛擬機(jī)是如何捕獲異常的?

 古明地覺O_o 2024-11-11 發(fā)布于北京

楔子

上一篇文章我們介紹了 Python 的異常是怎么實(shí)現(xiàn)的,拋出異常這個(gè)動作在虛擬機(jī)層面上是怎樣的一個(gè)行為,以及虛擬機(jī)在處理異常時(shí)候的棧幀展開行為。

既然虛擬機(jī)內(nèi)建的異常處理動作我們已經(jīng)了解了,那么接下來就看看異常捕獲是如何實(shí)現(xiàn)的,還有它又是如何影響虛擬機(jī)的異常處理流程的。畢竟在大部分情況下,我們都不會將異常拋出去,而是將它捕獲起來。


異常捕獲語句

這里先來回顧一下異常捕獲語句,首先一個(gè)完整的異常捕獲語句如下。

try:
    pass
except IndexError as e:
    pass
except Exception as e:
    pass
else:
    pass
finally:
    pass 

情況可以分為以下幾種:

1)如果 try 里面的代碼在執(zhí)行時(shí)沒有出現(xiàn)異常,那么會執(zhí)行 else ,然后執(zhí)行 finally。

try:
    print("我是 try")
except Exception as e:
    print("我是 except")
else:
    print("我是 else")
finally:
    print("我是 finally")
"""
我是 try
我是 else
我是 finally
"""
    

2)如果 try 里面的代碼在執(zhí)行時(shí)出現(xiàn)異常了(異常會被設(shè)置在線程狀態(tài)對象中),那么會依次判斷 except(可以有多個(gè))能否匹配發(fā)生的異常。如果某個(gè) except 將異常捕獲了,那么會將異常給清空,然后執(zhí)行 finally。

try:
    raise IndexError("IndexError Occurred")
except ValueError as e:
    print("ValueError 匹配上了異常")
except IndexError as e:
    print("IndexError 匹配上了異常")
except Exception as e:
    print("Exception 匹配上了異常")
else:
    print("我是 else")
finally:
    print("我是 finally")
"""
IndexError 匹配上了異常
我是 finally
"""
   

except 子句可以有很多個(gè),發(fā)生異常時(shí)會從上往下依次匹配。但是注意:多個(gè) except 子句最多只有一個(gè)被執(zhí)行,比如當(dāng)前的 IndexError 和 Exception 都能匹配發(fā)生的異常,但只會執(zhí)行匹配上的第一個(gè) except 子句。

另外只要發(fā)生異常了,else 就不會執(zhí)行了。不管 except 有沒有將異常捕獲到,都不會執(zhí)行 else,因?yàn)?else 只有在 try 里面沒有發(fā)生異常的時(shí)候才會執(zhí)行。

3)如果 try 里面的代碼在執(zhí)行時(shí)出現(xiàn)異常了,但 except 沒有將異常捕獲掉,那么異常仍然被保存在線程狀態(tài)對象中,然后執(zhí)行 finally。如果 finally 子句中沒有出現(xiàn) return、break、continue 等關(guān)鍵字,再將異常拋出來。

try:
    raise IndexError("IndexError Occurred")
except ValueError:
    print("ValueError 匹配上了異常")
finally:
    print("我是 finally")
"""
我是 finally
Traceback (most recent call last):
  File "......", line 2, in <module>
    raise IndexError("IndexError Occurred")
IndexError: IndexError Occurred
"""

except 沒有將異常捕獲掉,所以執(zhí)行完 finally 之后,異常又被拋出來了。但如果 finally 里面出現(xiàn)了 return、break、continue 等關(guān)鍵字,也不會拋出異常,而是將異常丟棄掉。

def f():
    try:
        raise IndexError("IndexError Occurred")
    except ValueError:
        print("ValueError 匹配上了異常")
    finally:
        print("我是 finally")
        return

f()
"""
我是 finally
"""


def g():
    for i in range(3):
        try:
            raise IndexError("IndexError Occurred")
        except ValueError:
            print("ValueError 匹配上了異常")
        finally:
            print(f"我是 finally,i = {i}")
            continue

g()
"""
我是 finally,i = 0
我是 finally,i = 1
我是 finally,i = 2
"""

由于 finally 里面出現(xiàn)了 return 和 continue,所以異常并沒有發(fā)生,而是被丟棄掉了。這個(gè)特性相信有很多小伙伴之前還是沒有發(fā)現(xiàn)的。

然后 try、except、else、finally 這幾個(gè)關(guān)鍵字不需要同時(shí)出現(xiàn),可以有以下幾種組合。

try ... except

try ... finally

try ... except ... else

try ... except ... else ... finally

注意里面的 except,可以出現(xiàn)多次,但其它關(guān)鍵字在一個(gè) try 語句內(nèi)只能出現(xiàn)一次。


返回值問題

如果這幾個(gè)關(guān)鍵字對應(yīng)的代碼塊都指定了返回值,那么聽誰的呢?下面解釋一下。

def retval():
    try:
        return 123
    except Exception:
        return 456

print(retval())  # 123

由于沒有發(fā)生異常,所以返回了 try 指定的返回值。

def retval():
    try:
        return 123
    except Exception:
        return 456
    else:
        return 789

print(retval())  # 123

雖然指定了 else,但是 try 里面已經(jīng)執(zhí)行 return 了,所以打印的仍是 try 的返回值。

def retval():
    try:
        1 / 0
        return 123
    except Exception:
        return 456

print(retval())  # 456

由于發(fā)生異常,所以返回了 except 指定的返回值。

def retval():
    try:
        1 / 0
        return 123
    except Exception:
        return 456
    else:
        return 789

print(retval())  # 456

一旦發(fā)生異常,else 就不可能執(zhí)行,所以此時(shí)仍然返回 456。

def retval():
    try:
        return 123
    except Exception:
        return 456
    finally:
        pass

print(retval())  # 123

finally 永遠(yuǎn)會執(zhí)行,但它沒有指定返回值,所以此時(shí)返回的是 123。

def retval():
    try:
        return 123
    except Exception:
        return 456
    finally:
        return

print(retval())  # None

一旦 finally 中出現(xiàn)了 return,那么返回的都是 finally 指定的返回值。并且此時(shí)即便出現(xiàn)了沒有捕獲的異常,也不會報(bào)錯(cuò),因?yàn)闀惓G棄掉。

def retval():
    try:
        return 123
    except Exception:
        return 456
    finally:
        pass
    return 789

print(retval())  # 123

函數(shù)一旦 return,就表示要返回了,但如果這個(gè) return 是位于出現(xiàn)了 finally 的異常捕獲語句中,那么會先執(zhí)行 finally,然后再返回。所以最后的 return 789 是不會執(zhí)行的,因?yàn)橐呀?jīng)出現(xiàn) return 了,finally 執(zhí)行完畢之后就直接返回了。

def retval():
    try:
        pass
    except Exception:
        return 456
    finally:
        pass
    return 789

print(retval())  # 789

沒有異常,所以 except 里的 return 不會執(zhí)行,而 try 和 finally 里面也沒有 return,因此返回 789。

一個(gè)簡單的異常捕獲,總結(jié)起來還稍微有點(diǎn)繞呢。

從 Python 的層面理解完異常捕獲之后,再來看看虛擬機(jī)是如何實(shí)現(xiàn)這一機(jī)制的?想要搞清楚這一點(diǎn),還是得從字節(jié)碼入手。


異常捕獲對應(yīng)的字節(jié)碼

隨便寫一段代碼,然后反編譯一下。

import dis

code_string = """
try:
    raise Exception("拋出一個(gè)異常")
except Exception as e:
    print(e)
finally:
    print("我一定會被執(zhí)行的")
"""


dis.dis(compile(code_string, "exception""exec"))

拋異常有兩種方式,一種是虛擬機(jī)執(zhí)行的時(shí)候出現(xiàn)錯(cuò)誤而拋出異常,另一種是使用 raise 關(guān)鍵字手動拋出異常。這里我們就用第二種方式,來看一下反編譯的結(jié)果(為了清晰,省略掉了源代碼行號)。

      0 RESUME                   0

      2 NOP

      4 PUSH_NULL
      6 LOAD_NAME                0 (Exception)
      8 LOAD_CONST               0 ('拋出一個(gè)異常')
     10 CALL                     1
     18 RAISE_VARARGS            1
>>   20 PUSH_EXC_INFO

     22 LOAD_NAME                0 (Exception)
     24 CHECK_EXC_MATCH
     26 POP_JUMP_IF_FALSE       18 (to 64)
     28 STORE_NAME               1 (e)

     30 PUSH_NULL
     32 LOAD_NAME                2 (print)
     34 LOAD_NAME                1 (e)
     36 CALL                     1
     44 POP_TOP
     46 POP_EXCEPT
     48 LOAD_CONST               1 (None)
     50 STORE_NAME               1 (e)
     52 DELETE_NAME              1 (e)
     54 JUMP_FORWARD             8 (to 72)
>>   56 LOAD_CONST               1 (None)
     58 STORE_NAME               1 (e)
     60 DELETE_NAME              1 (e)
     62 RERAISE                  1

>>   64 RERAISE                  0
>>   66 COPY                     3
     68 POP_EXCEPT
     70 RERAISE                  1

>>   72 NOP

     74 PUSH_NULL
     76 LOAD_NAME                2 (print)
     78 LOAD_CONST               2 ('我一定會被執(zhí)行的')
     80 CALL                     1
     88 POP_TOP
     90 RETURN_CONST             1 (None)
>>   92 PUSH_EXC_INFO
     94 PUSH_NULL
     96 LOAD_NAME                2 (print)
     98 LOAD_CONST               2 ('我一定會被執(zhí)行的')
    100 CALL                     1
    108 POP_TOP
    110 RERAISE                  0
>>  112 COPY                     3
    114 POP_EXCEPT
    116 RERAISE                  1
ExceptionTable:
  4 to 18 -> 20 [0]
  20 to 28 -> 66 [1] lasti
  30 to 44 -> 56 [1] lasti
  46 to 54 -> 92 [0]
  56 to 64 -> 66 [1] lasti
  66 to 70 -> 92 [0]
  92 to 110 -> 112 [1] lasti

字節(jié)碼指令還是比較多的,我們來分段解釋。


try 子句的指令

try 子句的指令如下。

      6 LOAD_NAME                0 (Exception)
      8 LOAD_CONST               0 ('拋出一個(gè)異常')
     10 CALL                     1
     18 RAISE_VARARGS            1

6 LOAD_NAME 指令會將 <class 'Exception'> 壓入運(yùn)行時(shí)棧。8 LOAD_CONST 指令會將字符串常量壓入運(yùn)行時(shí)棧。然后 10 CALL 指令將運(yùn)行時(shí)棧里的元素彈出,進(jìn)行調(diào)用。可以看到不管是調(diào)用函數(shù),還是調(diào)用類,執(zhí)行的都是 CALL 指令,然后將返回值(這里就是 Exception 對象,即異常)壓入棧中。

接著執(zhí)行 18 RAISE_VARARGS,這是一條新指令,看一下它的邏輯。

TARGET(RAISE_VARARGS) {
    PyObject **args = (stack_pointer - oparg);
    #line 606 "Python/bytecodes.c"
    PyObject *cause = NULL, *exc = NULL;
    switch (oparg) {
    case 2:
        cause = args[1];
        /* fall through */
    case 1:
        exc = args[0];
        /* fall through */
    case 0:
        // 調(diào)用 do_raise 設(shè)置異常
        if (do_raise(tstate, exc, cause)) {
            assert(oparg == 0);
            monitor_reraise(tstate, frame, next_instr-1);
            goto exception_unwind;
        }
        break;
    default:
        _PyErr_SetString(tstate, PyExc_SystemError,
                         "bad RAISE_VARARGS oparg");
        break;
    }
    if (true) { STACK_SHRINK(oparg); goto error; }
    #line 912 "Python/generated_cases.c.h"
}

因?yàn)?nbsp;RAISE_VARARGS 指令的參數(shù)是 1,所以 case 1 成立,于是將異常從運(yùn)行時(shí)棧中彈出,并賦值給變量 exc,然后調(diào)用 do_raise 函數(shù)。

在 do_raise 中,最終會調(diào)用之前說過的 PyErr_Restore 函數(shù),將異常對象存儲到當(dāng)前的線程狀態(tài)對象中,然后跳轉(zhuǎn)到標(biāo)簽為 exception_unwind 的地方開始異常捕獲。

exception_unwind:
{
    // INSTR_OFFSET 是一個(gè)宏,定義在 Python/ceval_macros.h 中
    // #define INSTR_OFFSET() ((int)(next_instr - _PyCode_CODE(frame->f_code)))
    int offset = INSTR_OFFSET()-1;
    int level, handler, lasti;
    // 查詢 co_exceptiontable,即異常處理表
    // 當(dāng) try 里面產(chǎn)生異常時(shí),那么必須跳轉(zhuǎn)到相應(yīng)的 except 或 finally 里面
    // 在 Python 3.10 以及之前的版本,這個(gè)機(jī)制是通過引入一個(gè)獨(dú)立的動態(tài)棧,跟蹤 try 語句塊實(shí)現(xiàn)的
    // 但從 3.11 開始,動態(tài)棧被替換成了靜態(tài)表,即異常處理表,由 co_exceptiontable 字段維護(hù)
    // 并且表在編譯期間就靜態(tài)生成了,是一段字節(jié)序列,里面包含了 try / except / finally 信息
    // 當(dāng)代碼在執(zhí)行過程中出現(xiàn)異常時(shí),解釋器會查詢這張表,尋找與之匹配的 except / finall 塊
    if (get_exception_handler(frame->f_code, offset, &level, &handler, &lasti) == 0
    {
        // No handlers, so exit.
        // ...
        // 如果 get_exception_handler 返回值等于 0,說明沒有找到
        // 那么跳轉(zhuǎn)到 exit_unwind 標(biāo)簽,退出當(dāng)前棧幀
        goto exit_unwind;
    }
    // 否則說明找到了,那么要進(jìn)行跳轉(zhuǎn),而跳轉(zhuǎn)地址保存在 handler 中
    assert(STACK_LEVEL() >= level);
    // ...
    // 獲取 tstate->current_exception,即當(dāng)前線程狀態(tài)對象保存的異常
    PyObject *exc = _PyErr_GetRaisedException(tstate);
    // 壓入棧中
    PUSH(exc);
    // 跳轉(zhuǎn)到指定指令,即 except / finally 塊對應(yīng)的指令
    JUMPTO(handler);
    if (monitor_handled(tstate, frame, next_instr, exc) < 0) {
        goto exception_unwind;
    }
    /* Resume normal execution */
    DISPATCH();
}

該指令執(zhí)行后,異常會被壓入棧中,虛擬機(jī)也知道該跳轉(zhuǎn)到什么地方了。


except 子句的指令

try 子句的指令我們說完了,再來看看 except 子句。

>>   20 PUSH_EXC_INFO
     22 LOAD_NAME                0 (Exception)
     24 CHECK_EXC_MATCH
     26 POP_JUMP_IF_FALSE       18 (to 64)
     28 STORE_NAME               1 (e)

     30 PUSH_NULL
     32 LOAD_NAME                2 (print)
     34 LOAD_NAME                1 (e)
     36 CALL                     1
     44 POP_TOP
     46 POP_EXCEPT
     48 LOAD_CONST               1 (None)
     50 STORE_NAME               1 (e)
     52 DELETE_NAME              1 (e)
     54 JUMP_FORWARD             8 (to 72)

首先執(zhí)行 20 PUSH_EXC_INFO 指令,內(nèi)部邏輯如下。

TARGET(PUSH_EXC_INFO) {
    // RAISE_VARARGS 指令將異常設(shè)置在了線程狀態(tài)對象中
    // 然后跳轉(zhuǎn)到 exception_unwind 標(biāo)簽,將異常壓入運(yùn)行時(shí)棧
    PyObject *new_exc = stack_pointer[-1];
    PyObject *prev_exc;
    #line 2543 "Python/bytecodes.c"
    /* tstate->current_exception 表示當(dāng)前存儲的異常
     * tstate->exc_info 是一個(gè)結(jié)構(gòu)體,相當(dāng)于對異常做了一個(gè)封裝
     *
     * typedef struct _err_stackitem {
     *     PyObject *exc_value;
     *     struct _err_stackitem *previous_item;
     * } _PyErr_StackItem;
     */

    _PyErr_StackItem *exc_info = tstate->exc_info;
    // exc_info->exc_value 還是之前存儲的異常
    if (exc_info->exc_value != NULL) {
        prev_exc = exc_info->exc_value;
    }
    else {
        prev_exc = Py_None;
    }
    assert(PyExceptionInstance_Check(new_exc));
    // 將 exc_info->exc_value 替換為新產(chǎn)生的異常
    exc_info->exc_value = Py_NewRef(new_exc);
    #line 3584 "Python/generated_cases.c.h"
    // 此時(shí)棧里面有兩個(gè)元素,從棧底到棧頂分別是舊異常、新異常
    STACK_GROW(1);
    stack_pointer[-1] = new_exc;
    stack_pointer[-2] = prev_exc;
    DISPATCH();
}

該指令做的事情是將舊異常和新異常壓入運(yùn)行時(shí)棧。

22 LOAD_NAME 加載 Exception,然后執(zhí)行 24 CHECK_EXC_MATCH,邏輯如下。

TARGET(CHECK_EXC_MATCH) {
    // 獲取棧頂元素,由于源碼中是 except Exception as e
    // 所以會得到上一條 LOAD_NAME 指令壓入的 class Exception
    PyObject *right = stack_pointer[-1];
    // 執(zhí)行 exception_unwind 標(biāo)簽內(nèi)的邏輯時(shí)壓入的異常
    PyObject *left = stack_pointer[-2];
    PyObject *b;
    #line 2122 "Python/bytecodes.c"
    // ...
    // 判斷異常對象和指定的異常類能否匹配
    int res = PyErr_GivenExceptionMatches(left, right);
    #line 2981 "Python/generated_cases.c.h"
    Py_DECREF(right);
    #line 2130 "Python/bytecodes.c"
    b = res ? Py_True : Py_False;
    #line 2985 "Python/generated_cases.c.h"
    // 將棧頂元素用 res 替換掉,此時(shí)棧里面有兩個(gè)元素
    // 從棧底到棧頂分別是:舊異常對象,新異常對象,布爾值(異常是否可以被捕獲)
    stack_pointer[-1] = b;
    DISPATCH();
}

26 POP_JUMP_IF_FALSE 會彈出棧頂元素,如果為 False,說明異常無法被捕獲,那么要跳轉(zhuǎn)到下一個(gè) except 或者 finally。如果可以被捕獲,那么執(zhí)行 28 STORE_NAME,再將棧里的異常對象彈出,賦值給變量 e。

到此 except Exception as e 這一行語句便已經(jīng)完成,至于接下來的 4 條指令應(yīng)該不需要解釋了。

很好理解,這 4 條就是 print(e) 對應(yīng)的指令,然后執(zhí)行 46 POP_EXCEPT,邏輯如下。

TARGET(POP_EXCEPT) {
    // 此時(shí)運(yùn)行時(shí)棧里面還剩下一個(gè)舊異常
    PyObject *exc_value = stack_pointer[-1];
    #line 930 "Python/bytecodes.c"
    _PyErr_StackItem *exc_info = tstate->exc_info;
    // 更新引用計(jì)數(shù)
    Py_XSETREF(exc_info->exc_value, exc_value);
    #line 1274 "Python/generated_cases.c.h"
    STACK_SHRINK(1);
    DISPATCH();
}

但是接下來的幾條指令是干嘛的,估計(jì)有人會感到困惑。

這幾條指令的具體作用,我們稍后解釋。


異常跳轉(zhuǎn)表

finally 子句對應(yīng)的指令比較簡單,我們就不看了。相比之前版本,3.12 的異常捕獲變得簡單許多,因?yàn)橄嚓P(guān)信息都靜態(tài)化了。在以前的版本中是使用 SETUP_FINALLY 等指令來處理異常,而 3.12 換成了更高效的異常表結(jié)構(gòu),類似于 Java 的異常表。

我們來看一下異常表的結(jié)構(gòu),它由 PyCodeObject 對象的 co_exceptiontable 字段負(fù)責(zé)維護(hù)。

4 to 18 -> 20 

表示 try 子句內(nèi)部對應(yīng)偏移量為 4 ~ 18 的指令,并且如果出現(xiàn)異常,那么跳轉(zhuǎn)到偏移量為 20 的指令。

20 to 28 -> 66 

偏移量為 20 ~ 28 的指令對應(yīng) except 子句本身,如果執(zhí)行出錯(cuò),跳轉(zhuǎn)到偏移量為 66 的指令去清理異常。

30 to 44 -> 56

偏移量為 30 ~ 44 的指令對應(yīng) except 子句內(nèi)部的處理邏輯,如果執(zhí)行出錯(cuò)則跳轉(zhuǎn)到第 56 條指令。

注意里面的 DELETE_NAME,它是 del 語句對應(yīng)的指令。所以跳轉(zhuǎn)之后的這幾條指令,負(fù)責(zé)刪除變量 e,怎么理解呢?我們舉個(gè)例子。

e = 2.71

try:
    raise Exception("xx")
except Exception as e:
    pass

print(e)
"""
NameError: name 'e' is not defined
"""

奇怪,為什么在外面打印變量 e 報(bào)錯(cuò)了呢?其實(shí) Python 會對 except 子句內(nèi)部做一些處理,以上代碼最終會變成下面這個(gè)樣子。

e = 2.71

try:
    raise Exception("xx")
except Exception as e:
    e = None
    try:
        pass
    finally:
        del e
finally:
    print(e)

至于這么做的原因,稍后解釋。

46 to 54 -> 92

del e 相關(guān)指令,但它對應(yīng)的是存在 finally 的情況,刪除之后會跳轉(zhuǎn)到偏移量為 92 的指令。

56 to 64 -> 66

del e 相關(guān)指令,對應(yīng)不存在 finally 的情況。

66 to 70 -> 92

異常清理相關(guān)指令。

92 to 110 -> 112

finally 子句對應(yīng)的指令。

我們看到  try / except / finally 塊的范圍信息、異常處理的起始位置、需要執(zhí)行的清理操作都被靜態(tài)化了,在編譯階段就已經(jīng)確定,所以性能方面比之前要更高。并且當(dāng) try 子句內(nèi)的代碼沒有出現(xiàn)錯(cuò)誤時(shí),和不使用異常捕獲之間基本沒有性能差異。

總之 Python 中一旦出現(xiàn)異常了,那么會將異常類型、異常值、異?;厮輻TO(shè)置在線程狀態(tài)對象中,然后棧幀一步一步地回退,尋找異常捕獲代碼(從內(nèi)向外)。如果退到了模塊級別還沒有發(fā)現(xiàn)異常捕獲,那么從外向內(nèi)打印 traceback 中的信息,當(dāng)走到最內(nèi)層的時(shí)候再將線程中設(shè)置的異常類型和異常值打印出來。

def h():
    1 / 0

def g():
    h()

def f():
    g()

f()

# traceback 回溯棧
Traceback (most recent call last):
  # 打印模塊的 traceback
  # 并提示:發(fā)生錯(cuò)誤是因?yàn)樵诘?10 行調(diào)用了 f()
  File "/Users/.../main.py", line 10in <module>
    f()

  # 打印函數(shù) f 的 traceback
  # 并提示:發(fā)生錯(cuò)誤是因?yàn)樵诘?8 行調(diào)用了 g()
  File "/Users/.../main.py", line 8in f
    g()

  # 打印函數(shù) g 的 traceback
  # 并提示:發(fā)生錯(cuò)誤是因?yàn)樵诘?5 行調(diào)用了 h()
  File "/Users/.../main.py", line 5in g
    h()

  # 打印函數(shù) h 的 traceback
  # 并提示:發(fā)生錯(cuò)誤是因?yàn)樵诘?2 行執(zhí)行了 1 / 0
  File "/Users/.../main.py", line 2in h
    1 / 0

# 函數(shù) h 的 traceback -> tb_next 為 None,證明錯(cuò)誤是發(fā)生在函數(shù) h 中
# 在模塊中調(diào)用函數(shù) f 相當(dāng)于導(dǎo)火索,然后一層一層輸出,最終定位到函數(shù) h
# 然后再將之前設(shè)置在線程狀態(tài)對象中的異常類型和異常值打印出來即可
ZeroDivisionError: division by zero

模塊中調(diào)用了函數(shù) f,函數(shù) f 調(diào)用了函數(shù) g,函數(shù) g 調(diào)用了函數(shù) h。然后在函數(shù) h 中執(zhí)行出錯(cuò)了,但又沒有異常捕獲,那么會將執(zhí)行權(quán)交給函數(shù) g 對應(yīng)的棧幀,但是函數(shù) g 也沒有異常捕獲,那么再將執(zhí)行權(quán)交給函數(shù) f 對應(yīng)的棧幀。所以調(diào)用的時(shí)候棧幀一層一層創(chuàng)建,當(dāng)執(zhí)行完畢或者出現(xiàn)異常時(shí),棧幀再一層層回退。

因此棧幀的遍歷順序是從函數(shù) h 到模塊,traceback 的遍歷順序是從模塊到函數(shù) h。


為什么要執(zhí)行 del

前面說了,在 except 語句塊內(nèi),如果將異常賦給了某個(gè)變量,那么 except 結(jié)束時(shí)會將變量刪掉。

e = 2.71

def get_e():
    return e

try:
    raise Exception("我要引發(fā)異常了")
except Exception as e:
    # 因?yàn)?nbsp;except Exception as e 位于全局作用域
    # 所以執(zhí)行完之后,全局變量 e 就被修改了
    print(get_e())  # 我要引發(fā)異常了
    # 但是在最后還會隱式地執(zhí)行 del e,那為什么要這么做呢?
    # 因?yàn)?nbsp;except 子句結(jié)束后,變量 e 指向的異常對象就沒用了
    # 而如果不 del e 的話,那么異常對象不會被銷毀
    # 此外還有一個(gè)原因,通過 __traceback__ 可以拿到當(dāng)前的回溯棧,即 traceback 對象
    print(e.__traceback__)  # <traceback object at 0x104a98b80>
    # 而 traceback 對象保存當(dāng)前的棧幀,然后棧幀又保存了包含變量 e 的名字空間
    print(e.__traceback__.tb_frame.f_locals["e"is e)  # True
    # 相信你能猜到這會帶來什么后果,沒錯(cuò),就是循環(huán)引用
    # 因此在 except 結(jié)束時(shí)會隱式地 del e

# 顯然當(dāng) except 結(jié)束后,全局變量 e 就無法訪問了
print(e)
"""
NameError: name 'e' is not defined
"""

所以在附加了回溯信息的情況下,它們會形成堆棧幀的循環(huán)引用,在下一次垃圾回收執(zhí)行之前,會使所有變量都保持存活。


小結(jié)

本篇文章我們就分析了異常捕獲的實(shí)現(xiàn)原理,總的來說并不難,因?yàn)樗械男畔⒍检o態(tài)保存在了異常跳轉(zhuǎn)表(簡稱異常表)中。并且在不報(bào)錯(cuò)時(shí),異常捕獲對程序性能沒有任何影響,所以放心使用。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多