《深度剖析CPython解釋器》13. 剖析Python的流程控制語句(if、for、while),以及異常捕獲機制


楔子

在上一章中,我們介紹了Python虛擬機中常見的字節碼指令。但我們的流程都是從上往下順序執行的,在執行的過程中沒有任何變化,但是顯然這是不夠的,因為怎么能沒有流程控制呢。下面我們來看看Python所提供的流程控制手段,其中也包括異常檢測機制。

Python虛擬機中的if控制流

if字節碼

if語句算是最簡單也是最常用的控制流語句,那么它的字節碼是怎么樣的呢?當然我們這里的if語句指的是if、elif、elif...、else整體,里面的if、某個elif或者else叫做該if語句的分支。

s = """
gender = "男"

if gender == "男":
    print("nice muscle")
elif gender == "女":
    print("白い肌")
else:
    print("秀吉")
"""


if __name__ == '__main__':
    import dis
    dis.dis(compile(s, "man", "exec"))

反編譯得到的字節碼指令比較多,我們來慢慢分析。

注意:到了現在,相信對字節碼指令都已經熟悉了,因此之前說過的指令我們就不詳細展開說了,只會簡單提一下。

  2           0 LOAD_CONST               0 ('男') //加載字符串常量
              2 STORE_NAME               0 (gender)//建立符號和對象的映射關系

  4           4 LOAD_NAME                0 (gender)//加載變量gender
              6 LOAD_CONST               0 ('男')//加載字符串常量
              8 COMPARE_OP               2 (==)//將gender和"男"進行==操作
             10 POP_JUMP_IF_FALSE       22//這里的22表示如果為False, 就跳轉到字節碼偏移量、或者字節碼的索引為22的地方
      					  //顯然是下面的22 LOAD_NAME 0 (gender), 即:該if下面的elif

  5          12 LOAD_NAME                1 (print) //如果gender == "男"成立, 那么不會跳轉, 直接往下執行, 加載符號print
             14 LOAD_CONST               1 ('nice muscle')//加載字符串常量
             16 CALL_FUNCTION            1//函數調用
             18 POP_TOP	//將函數返回值從棧頂彈出去
             20 JUMP_FORWARD            26 (to 48)//if語句只會執行一個分支, 一旦執行了某個分支, 整個if語句就結束了
      					          //所以跳轉到字節碼偏移量為48的位置, 這里的22就表示相對於當前位置向前跳轉了多少

  6     >>   22 LOAD_NAME                0 (gender) //顯然這是elif的分支, 加載變量gender
             24 LOAD_CONST               2 ('女')//加載字符串常量"女"
             26 COMPARE_OP               2 (==)//將gender和"女"進行==判斷
             28 POP_JUMP_IF_FALSE       40//如果不成立就跳轉到字節碼偏移量為40的地方, 顯然是elif下面的else
      					  //如果elif下面還有elif, 那么就跳轉到下一個elif, 總之就是一個分支一個分支的往下跳轉

  7          30 LOAD_NAME                1 (print)//走到這里說明gender == "女"成立, 加載變量print
             32 LOAD_CONST               3 ('白い肌')//加載字符串常量"白い肌"
             34 CALL_FUNCTION            1//函數調用, 參數為1個
             36 POP_TOP//將函數返回值從棧頂彈出去
             38 JUMP_FORWARD             8 (to 48)//整個if語句結束, 還是跳轉到字節碼偏移量為48的位置
      						  //這里參數是8, 所以if的跳轉是采用相對跳躍, 分支不同跳躍的指令數也不同

  9     >>   40 LOAD_NAME                1 (print) //走到這里說明執行的是else分支, 加載符號print
             42 LOAD_CONST               4 ('秀吉')//加載字符串常量"秀吉"
             44 CALL_FUNCTION            1//函數調用
             46 POP_TOP//將函數返回值從棧頂彈出去,如果是執行else分支並且執行完畢, 顯然就不需要再跳轉了,
      		       //因為else分支位於整個if語句的最后面
      
        >>   48 LOAD_CONST               5 (None)//這里便是整個if語句結束后的第一條指令, 加載常量None
             50 RETURN_VALUE//返回

我們看到字節碼中 "源代碼行號" 和 "字節碼偏移量" 之間有幾個>>這樣的符號,這是什么呢?仔細看一下應該就知道,這顯然就是if語句中的每一個分支開始的地方,當然最后的>>是返回值。

但是經過分析,我們發現整個if語句的字節碼指令還是很簡單的。從上到下執行分支,如果某個分支成立,就執行該分支的代碼,執行完畢后直接跳轉到整個if語句下面的第一條指令;分支不成立那么就跳轉到下一個分支。

核心指令就在於COMPARE_OP、POP_JUMP_IF_FALSE和JUMP_FORWARD,從結構上我們不難分析:

  • COMPARE_OP: 進行比較操作
  • POP_JUMP_IF_FALSE: 跳轉到下一個分支
  • JUMP_FORWARD:跳轉到整個if語句結束后的第一條指令

我們首先分析COMPARE_OP,我們看到COMPARE_OP后面也是有參數的,比如 8 COMPARE_OP 2 (==),顯然oparg`(字節碼指令參數)`就是2,那么這個2代表啥呢?其實想都不用想,肯定代表的是==,因為都已經告訴我們了。

// object.h
/* Rich comparison opcodes */
#define Py_LT 0 //小於
#define Py_LE 1 //小於等於
#define Py_EQ 2 //等於
#define Py_NE 3 //不等於
#define Py_GT 4 //大於
#define Py_GE 5 //大於等於


//opcode.h
enum cmp_op {PyCmp_LT=Py_LT, PyCmp_LE=Py_LE, PyCmp_EQ=Py_EQ, PyCmp_NE=Py_NE,
                PyCmp_GT=Py_GT, PyCmp_GE=Py_GE, PyCmp_IN, PyCmp_NOT_IN,
                PyCmp_IS, PyCmp_IS_NOT, PyCmp_EXC_MATCH, PyCmp_BAD};

下面我們來看看,虛擬機中是如何進行比較操作的。另外本章中如果沒有指定源碼位置,那么默認是在Python/ceval.c里面

static PyObject *
cmp_outcome(int op, PyObject *v, PyObject *w)
{
    int res = 0;
    switch (op) {
    //python中的is, 在C的層面直接判斷兩個指針是否相等即可        
    case PyCmp_IS:
        res = (v == w);
        break;
    //python中的is not, , 在C的層面直接判斷兩個指針是否不相等即可    
    case PyCmp_IS_NOT:
        res = (v != w);
        break;
    //python中的in, 調用PySequence_Contains
    case PyCmp_IN:
        res = PySequence_Contains(w, v);
        if (res < 0)
            return NULL;
        break;
    //python中的not in, 調用PySequence_Contains再取反        
    case PyCmp_NOT_IN:
        res = PySequence_Contains(w, v);
        if (res < 0)
            return NULL;
        res = !res;
        break;
    //python中的異常      
    case PyCmp_EXC_MATCH:
        //這里判斷給定的類是不是異常類, 比如我們肯定不能except int as e, 異常類一定要繼承BaseException
        //如果是元組的話, 那么元組里面都要是異常類
        if (PyTuple_Check(w)) {
            Py_ssize_t i, length;
            length = PyTuple_Size(w);
            for (i = 0; i < length; i += 1) {
                PyObject *exc = PyTuple_GET_ITEM(w, i);
                if (!PyExceptionClass_Check(exc)) {
                    PyErr_SetString(PyExc_TypeError,
                                    CANNOT_CATCH_MSG);
                    return NULL;
                }
            }
        }
        else {
            if (!PyExceptionClass_Check(w)) {
                PyErr_SetString(PyExc_TypeError,
                                CANNOT_CATCH_MSG);
                return NULL;
            }
        }
        //判斷指定的異常能否捕獲相應的錯誤
        res = PyErr_GivenExceptionMatches(v, w);
        break;
    default:
        //然后進行比較操作, 傳入兩個對象以及操作符, 即上面的Py_LT、Py_LE...之一
        return PyObject_RichCompare(v, w, op);
    }
    //Py_True和Py_False就相當於Python中的True和False, 本質上是一個PyLongObject
    //根據res的結果返回True和False
    v = res ? Py_True : Py_False;
    Py_INCREF(v);
    return v;
}

里面的比較函數PyObject_RichCompare很重要,我們來看一下,該函數位於Object/object.c中。

//首先有一個PyObject_RichCompareBool, 它是用來判斷兩個對象是否相等或不等的
int
PyObject_RichCompareBool(PyObject *v, PyObject *w, int op)
{
    PyObject *res;
    int ok;

    //如果v和w相等的話, 說明這兩個變量指向同一個對象
    if (v == w) {
        //那么如果op是==, 顯然返回True
        if (op == Py_EQ)
            return 1;
        //如果op是!=, 顯然返回False
        else if (op == Py_NE)
            return 0;
    }
    //可能有人問如果我們重寫了__eq__怎么辦? 所以這個方法只適用於內建的類的實例對象
    //如果是我們自定義的類會直接調用這里的PyObject_RichCompare
    //另外我們看到即便是內置的類的實例對象, 如果兩個對象不相等, 或者相等、但是op不是==和!=的時候也會走這里的PyObject_RichCompare
    res = PyObject_RichCompare(v, w, op);
    //通過PyObject_RichCompare進行比較
    if (res == NULL)
        return -1;
    //如果返回的是布爾值, 那么判斷是否和Py_True相等, 返回的是True那么比較的結果也是True, 否則是False
    if (PyBool_Check(res))
        ok = (res == Py_True);
    else
        //返回的不是布爾值, 那么調用PyObject_IsTrue, 顯然這相當於Python中的bool(res)
        //不是0的整數、長度不為0的字符串、元組、列表等等也是True
        ok = PyObject_IsTrue(res);
    Py_DECREF(res);
    //返回
    return ok;
}


//重點來了, 我們來看看PyObject_RichCompare
PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    PyObject *res;
    //首先會對op進行判斷, 要確保Py_LT <= op <= Py_GE, 即0 <= op <= 5, 要保證op是幾個操作符中的一個
    assert(Py_LT <= op && op <= Py_GE);
    //首先v和w不能是C的空指針, 要確保它們都指向一個具體的PyObject, 但是說實話底層的這些檢測我們在Python的層面基本不會遇到
    if (v == NULL || w == NULL) {
        if (!PyErr_Occurred())
            PyErr_BadInternalCall();
        return NULL;
    }
    //所以核心是下面的do_richcompare, 但是在do_richcompare之前我們看到這里調用了Py_EnterRecursiveCall
    //這和函數和遞歸有關, 比如我們在__eq__中又對self使用了==, 那么會不斷調用__eq__, 這是會無限遞歸的
    if (Py_EnterRecursiveCall(" in comparison"))
        //所以Py_EnterRecursiveCall是讓解釋器追蹤遞歸的深度的
	    //如果遞歸層數過多, 超過了指定限制(默認是999, 可以通過sys.getrecursionlimit()查看), 那么能夠及時拋出異常, 從遞歸中擺脫出來
        return NULL;
    
    //調用do_richcompare, 還是這三個參數, 得到比較的結果
    res = do_richcompare(v, w, op);
    //離開遞歸調用
    Py_LeaveRecursiveCall();
    //返回res, 執行PyObject_RichCompareBool中下面的邏輯
    return res;
}

//所以我們看到核心其實是do_richcompare, 我們需要繼續往下看
static PyObject *
do_richcompare(PyObject *v, PyObject *w, int op)
{
    richcmpfunc f; //富比較函數
    PyObject *res; //比較結果
    int checked_reverse_op = 0;
	
    //如果type(v)和type(w)不一樣 && type(w)是type(v)的子類 && type(w)中定義了tp_richcompare
    if (v->ob_type != w->ob_type &&
        PyType_IsSubtype(w->ob_type, v->ob_type) &&
        (f = w->ob_type->tp_richcompare) != NULL) {
        checked_reverse_op = 1;
        //那么直接調用type(w)的to_richcompare進行比較
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    //type(v)和type(w)不同, 或者 type(w) 不是 type(v) 的子類, 或者type(w)中沒有定義tp_richcompare
    //如果type(v)定義了tp_richcompare
    if ((f = v->ob_type->tp_richcompare) != NULL) {
        //調用type(v)的tp_richcompare方法
        res = (*f)(v, w, op);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    //type(w) 不是 type(v) 的子類 && type(v)中沒有定義tp_richcompare && type(w)中定義了tp_richcompare
    if (!checked_reverse_op && (f = w->ob_type->tp_richcompare) != NULL) {
        //那么執行w的tp_richcompare
        res = (*f)(w, v, _Py_SwappedOp[op]);
        if (res != Py_NotImplemented)
            return res;
        Py_DECREF(res);
    }
    //所以以上三種情況就相當於: 如果type(w) 是 type(v) 的子類, 那么優先調用w的tp_richcompare
    //否則,type(v) 和 type(w) 中誰的tp_richcompare不為空就調用誰的
    //如果都沒有那么就走下面的邏輯了
    switch (op) {
    //直接比較兩者是否相等
    case Py_EQ:
        res = (v == w) ? Py_True : Py_False;
        break;
    //比較兩者是否不等        
    case Py_NE:
        res = (v != w) ? Py_True : Py_False;
        break;
    //顯然此時的兩個對象只能判斷相等或者不等, 如果是比大小那么顯然是報錯的, 下面的信息你一定很熟悉
    default:
        PyErr_Format(PyExc_TypeError,
                     "'%s' not supported between instances of '%.100s' and '%.100s'",
                     opstrings[op],
                     v->ob_type->tp_name,
                     w->ob_type->tp_name);
        return NULL;
    }
    Py_INCREF(res);
    //返回
    return res;
}

另外,這里面又出現了tp_richcompare,如果我們自定義的類沒有重寫的話,那么默認調用的是基類object的tp_richcompare,包括內置的類也是調用object的tp_richcompare,有興趣可以看一下。

然后我們再來看看POP_JUMP_IF_FALSE

  2           0 LOAD_CONST               0 ('男')
              2 STORE_NAME               0 (gender)

  4           4 LOAD_NAME                0 (gender)
              6 LOAD_CONST               0 ('男')
              8 COMPARE_OP               2 (==)
             10 POP_JUMP_IF_FALSE       22
      
  5          12 LOAD_NAME                1 (print) 
             14 LOAD_CONST               1 ('nice muscle')
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_FORWARD            26 (to 48)

  6     >>   22 LOAD_NAME                0 (gender) 
             24 LOAD_CONST               2 ('女')
             26 COMPARE_OP               2 (==)
             28 POP_JUMP_IF_FALSE       40

  7          30 LOAD_NAME                1 (print)
             32 LOAD_CONST               3 ('白い肌')
             34 CALL_FUNCTION            1
             36 POP_TOP
             38 JUMP_FORWARD             8 (to 48)

  9     >>   40 LOAD_NAME                1 (print) 
             42 LOAD_CONST               4 ('秀吉')
             44 CALL_FUNCTION            1
             46 POP_TOP
      
        >>   48 LOAD_CONST               5 (None)
             50 RETURN_VALUE

我們看一下10 POP_JUMP_IF_FALSE 22這條字節碼,這表是if語句不成立,那么會跳轉到字節碼偏移量為22的位置,所以這里有一個指令跳躍的動作。那么Python虛擬機是如何完成指令跳躍的呢?關鍵就在於一個名為 predict 的宏里面。

#if defined(DYNAMIC_EXECUTION_PROFILE) || USE_COMPUTED_GOTOS
#define PREDICT(op)             if (0) goto PRED_##op
#else
#define PREDICT(op) \
    do{ \
        _Py_CODEUNIT word = *next_instr; \
        opcode = _Py_OPCODE(word); \
        if (opcode == op){ \
            oparg = _Py_OPARG(word); \
            next_instr++; \
            goto PRED_##op; \
        } \
    } while(0)
#endif
#define PREDICTED(op)           PRED_##op:

在Python中,有一些字節碼指令通常都是按照順序出現的,通過上一個字節碼指令直接預測下一個字節碼指令是可能的。比如COMPARE_OP的后面通常都會緊跟着POP_JUMP_IF_TRUE或者POP_JUMP_IF_FALSE,這在上面的字節碼中可以很清晰的看到。

為什么要有這樣的一個預測功能呢?因為當字節碼之間的指令搭配出現的概率非常高時,如果預測成功,能夠省去很多無謂的操作,使得執行效率大幅提高。我們可以看到, PREDICTED(POP_JUMP_IF_FALSE);實際上就是檢查下一條待處理的字節碼是否是POP_JUMP_IF_FALSE。如果是,那么程序會直接跳轉到PRED_POP_JUMP_IF_FALSE那里,如果將COMPARE_OP這個宏展開,可以看得更加清晰。

if (*next_instr == POP_JUMP_IF_FALSE)
    goto PRED_POP_JUMP_IF_FALSE;
if (*next_instr == POP_JUMP_IF_TRUE)
    goto PRED_POP_JUMP_IF_TRUE

但是問題又來了,PRED_POP_JUMP_IF_TRUE和PRED_POP_JUMP_IF_FALSE這些標識在哪里呢?我們知道指令跳躍的目的是為了繞過一些無謂的操作,直接進入POP_JUMP_IF_TRUE或者POP_JUMP_IF_FALSE指令對應的case語句之前。

首先if gender == "男"這條字節碼序列中,存在POP_JUMP_IF_FALSE指令,那么在COMPARE_OP指令的實現代碼的最后,將執行goto PRED_POP_JUMP_IF_FALSE;,而顯然這句代碼要在POP_JUMP_IF_FALSE之前執行。

 
        PREDICTED(POP_JUMP_IF_FALSE);
        TARGET(POP_JUMP_IF_FALSE) {
            //取出之前比較的結果。
            PyObject *cond = POP();
            int err;
            //比較結果為True,順序執行
            if (cond == Py_True) {
                Py_DECREF(cond);
                FAST_DISPATCH();
            }
            //比較結果為False,進行跳轉
            if (cond == Py_False) {
                Py_DECREF(cond);
                JUMPTO(oparg);
                FAST_DISPATCH();
            }
            //異常檢測
            err = PyObject_IsTrue(cond);
            Py_DECREF(cond);
            if (err > 0)
                ;
            else if (err == 0)
                JUMPTO(oparg);
            else
                goto error;
            DISPATCH();
        }

我們看到這里的調用跳轉使用的JUMPTO,在for循環中我們還會見到,這是一個宏。

#define JUMPTO(x)       (next_instr = first_instr + (x) / sizeof(_Py_CODEUNIT))
/*
_Py_CODEUNIT 是 uint16_t 的別名 typedef uint16_t _Py_CODEUNIT,占兩個字節; 
從名字也能看出這表示字節碼的指令單元, 一條指令兩個字節, 所以字節碼指令對應的偏移量是0 2 4 6 8..., 每次增加2

另外這里的first_str指向字節碼偏移量為0的位置, 也就是第一條指令
next_str表示在first_str基礎上跳轉之后的指令, 所以如果x是12的話, 那么next_str = 0 + 6, 顯然就是第7條指令
*/

Python虛擬機中的for循環控制流

我們在if語句中已經見識了最基本的控制,但是我們發現if里面只能向前,不管是哪個分支,都是通過JUMP_FORWARD。下面介紹for循環,我們會見到指令時可以回退的。但是在if語句的分支中,我們看到無論哪個分支、其指令的跳躍距離通常都是當前指令與目標指令的距離,相當於向前跳了多少步。那么指令回退時,是不是相當於向后跳了多少步呢?帶着疑問,我們來往下看。

for字節碼

我們來看看一個簡單的for循環的字節碼。

s = """
lst = [1, 2]
for item in lst:
    print(item)
"""


if __name__ == '__main__':
    import dis
    dis.dis(compile(s, "for", "exec"))
  2           0 LOAD_CONST               0 (1) //加載常量1
              2 LOAD_CONST               1 (2) //加載常量2
              4 BUILD_LIST               2     //構建PyListObject對象, 元素個數為2
              6 STORE_NAME               0 (lst) //使用符號"lst"保存

  3           8 LOAD_NAME                0 (lst) //加載變量lst
             10 GET_ITER					   //獲取對應的迭代器
        >>   12 FOR_ITER                12 (to 26)//開始for循環, 循環結束跳轉到字節碼偏移量為26的地方
             14 STORE_NAME               1 (item) //將元素迭代出來, 使用符號"item"保存

  4          16 LOAD_NAME                2 (print) //加載函數print
             18 LOAD_NAME                1 (item) //加載變量item
             20 CALL_FUNCTION            1        //函數調用
             22 POP_TOP						    //從棧頂彈出print函數的返回值, 這里是None
             24 JUMP_ABSOLUTE           12		 //for循環遍歷一圈之后, 繼續跳轉回去, 遍歷下一圈, 直到結束
        >>   26 LOAD_CONST               2 (None) //走到這里for循環就結束了, 加載常量None, 然后返回
             28 RETURN_VALUE

我們再來詳細分析一下上面的指令:

lst = [1, 2]我們就不分析了,當 for item in lst:的時候,肯定首先要找到lst,所以指令是LOAD_NAME是沒問題的。但是下面出現了GET_ITER,從字面上我們知道這是獲取迭代器,其實即使不從源碼的角度,我相信有的小伙伴對於for循環的機制也不是很了解。

實際上我們for循環遍歷一個對象的時候,首先要滿足后面的對象是一個可迭代對象,遍歷這個對象的時候,會先調用這個對象的__iter__方法,把它變成一個迭代器。然后不斷地調用這個迭代器的__next__方法,一步一步將里面的值全部迭代出來,然后再進行一次迭代出現StopIteration異常,for循環捕捉,然后退出。注意:for item in lst是先將lst對應的迭代器中的元素迭代出來,然后交給變量item。所以字節碼中先是12 FOR_ITER,然后才是14 STORE_NAME。因此10個元素的迭代器,是需要迭代11次才能結束的,因為Python不知道迭代10次就能結束,它需要再迭代一次發現沒有元素可以迭代、從而拋出StopIteration異常、再被for循環捕捉之后才能結束。

所以for循環后面如果跟的是一個迭代器,那么直接調用__next__方法,如果是可迭代對象,會先調用其內部的__iter__方法將其變成一個迭代器,然后再調用該迭代器的__next__方法。

from typing import Iterable, Iterator

lst = [1, 2]

# 列表、字符串、元組、字典、集合等等都是可迭代對象
# 但它們不是迭代器
print(isinstance(lst, Iterable))  # True
print(isinstance(lst, Iterator))  # False

# 需要調用__iter__之后才是一個迭代器, 當然迭代器也是可迭代對象
print(isinstance(iter(lst), Iterable))  # True
print(isinstance(iter(lst), Iterator))  # True

然后我們看到 24 JUMP_ABSOLUTE,它是跳轉到字節碼偏移量為12、也就是FOR_ITER的位置,並沒有跳到GET_ITER那里,所以for循環在遍歷的時候只會創建一次迭代器。

lst = [1, 2]

lst_iter = iter(lst)
for item in lst_iter:
    print(item, end=" ")  # 1 2

print()

for item in iter(lst):
    print(item, end=" ")  # 1 2
    
# 我們看到結果是一樣的, for item in iter(lst)和for item in lst是等價的
# 都會先創建迭代器, 並且只創建一次, 然后遍歷這個迭代器

list迭代器

Python虛擬機通過LOAD_NAME 0 (lst)指令,將剛創建的PyListObject對象壓入運行時棧。然后再通過GET_ITER指令來獲取PyListObject對象的迭代器。

        case TARGET(GET_ITER): {
            /* before: [obj]; after [getiter(obj)] */
            //從運行時棧獲取PyListObject對象
            PyObject *iterable = TOP();
            //獲取該PyListObject對象的iterator
            PyObject *iter = PyObject_GetIter(iterable);
            Py_DECREF(iterable);
            //將iterator壓入棧中, 設置在棧頂
            SET_TOP(iter);
            if (iter == NULL)
                goto error;
            PREDICT(FOR_ITER);
            PREDICT(CALL_FUNCTION);
            DISPATCH();
        }

我們看到獲取迭代器是調用了PyObject_GetIter函數,我們看看這個函數長什么樣子。

//Objects/object.h
typedef PyObject *(*getiterfunc) (PyObject *);

//Objects/abstract.c
PyObject *
PyObject_GetIter(PyObject *o)
{	
    //獲取對象的類型
    PyTypeObject *t = o->ob_type;
    //一個函數指針, 接收一個PyObject *, 返回一個PyObject *
    getiterfunc f;
	
    //調用類型對象的tp_iter
    f = t->tp_iter;
    if (f == NULL) {
        //如果f是NULL, 並且還不是序列型對象, 那么直接拋出異常, 'xxx' object is not iterable
        if (PySequence_Check(o))
            return PySeqIter_New(o);
        return type_error("'%.200s' object is not iterable", o);
    }
    else {
        //調用tp_iter, 傳入對象獲取迭代器。我們獲取迭代器是通過iter(lst)或者lst.__iter__()
        //但是在底層相當於list.__iter__(lst), 所以"實例.方法(*args, **kwargs)"等價於"類.函數(self, *args, **kargs)"
        PyObject *res = (*f)(o);
        //如果res不為空、並且還不是迭代器
        if (res != NULL && !PyIter_Check(res)) {
            //那么報錯TypeError, __iter__返回了一個非迭代器
            PyErr_Format(PyExc_TypeError,
                         "iter() returned non-iterator "
                         "of type '%.100s'",
                         res->ob_type->tp_name);
            Py_DECREF(res);
            res = NULL;
        }
        return res;
    }
}

因此我們可以看到,PyObject_GetIter是調用對象對應的類型對象中的tp_iter操作來獲取與對象關聯的迭代器的。我們說Python一切皆對象,那么這些迭代器也是一個實實在在的對象,那么也必然會有對應的類型對象,因為Python中對象對應的結構體都繼承了PyObject,所以任何一個對象都有引用計數和類型。

//listobject.c
typedef struct {
    PyObject_HEAD //迭代器顯然是不可變對象
    Py_ssize_t it_index; //迭代的元素的索引, 初始為0, 每迭代1個元素it_index就加1
    PyListObject *it_seq; //指向一個PyListObject對象, 顯然迭代的就是這個PyListObject對象里面的元素, 當元素迭代完畢之后it_seq會被設置成NULL
} listiterobject;


PyTypeObject PyListIter_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "list_iterator",                            /* tp_name */
	...
};

然后PyList_Type中tp_iter域被設置為list_iter,顯然這是PyObject_GetIter中的那個f,而這也正是創建迭代器的關鍵所在。

//listobject.c
static PyObject *
list_iter(PyObject *seq)
{	
    //列表對應的迭代器的指針
    listiterobject *it;
	
    //如果seq不是列表,則報錯
    if (!PyList_Check(seq)) {
        PyErr_BadInternalCall();
        return NULL;
    }
    //為listiterobject申請空間
    it = PyObject_GC_New(listiterobject, &PyListIter_Type);
    if (it == NULL)
        return NULL;
    //迭代器的索引, 用來遍歷列表的, 初始為0
    it->it_index = 0;
    Py_INCREF(seq);
    //這里的seq就是之前的PyListObject對象
    it->it_seq = (PyListObject *)seq;
    _PyObject_GC_TRACK(it);
    return (PyObject *)it;
}

可以看到PyListObject的迭代器對象只是對PyListObject對象做了一個簡單的包裝,在迭代器中,維護了迭代是要訪問的元素在PyListObject對象中的索引:it_index。通過這個索引,listiterobject對象就可以實現PyListObject的遍歷。

所以我們看到迭代器的實現真的很簡單,創建誰的迭代器就對誰進行一層包裝罷了,迭代器內部有一個索引。每迭代1次索引就加1,迭代完畢之后將指針設置為NULL,然后再迭代就拋出異常。

所以任何一個列表對應的迭代器的內存大小都是32字節,PyObject是16字節,再加上一個Py_ssize_t和一個指針,總共32字節。

s = "夏色祭" * 1000
print(s.__sizeof__(), iter(s).__sizeof__())  # 6074 32

lst = [1, 2, 3] * 1000
print(lst.__sizeof__(), iter(lst).__sizeof__())  # 24040 32

tpl = (1, 2, 3) * 1000
print(tpl.__sizeof__(), iter(tpl).__sizeof__())  # 24024 32

# 不光是列表, 包括字符串、元組也是一樣的, 都是32字節

但是字典有些特殊,因為它的底層是通過哈希表存儲的,它需要額外維護一些信息。

//Objects/dictobject.c
typedef struct {
    PyObject_HEAD
    PyDictObject *di_dict; /* Set to NULL when iterator is exhausted */
    Py_ssize_t di_used;
    Py_ssize_t di_pos;
    PyObject* di_result; /* reusable result tuple for iteritems */
    Py_ssize_t len;
} dictiterobject;

所以字典對應的迭代器是56字節,集合對應的迭代器則是48字節,關於集合可以去源碼中查看,看看為什么會占48字節。

d = dict.fromkeys(range(100000), None)
print(d.__sizeof__(), iter(d).__sizeof__())  # 5242952 56

s = set(range(100000))
print(s.__sizeof__(), iter(s).__sizeof__())  # 4194504 48

在指令GET_ITER完成之后,Python虛擬機開始了FOR_ITER指令的預測動作,如你所知,這樣的預測動作是為了提高執行的效率。

迭代控制

源代碼中的for循環,在虛擬機層面也一定對應着一個相應的循環控制結構。因為無論進行怎樣的變換,都不可能在虛擬機層面利用順序結構來實現源碼層面上的循環結構,這也可以看成是程序的拓撲不變性。顯然正如我們剛才分析的,當創建完迭代器之后,就正式開始進入for循環了,沒錯就是從FOR ITER開始,進入了Python虛擬機層面上的for循環。

        case TARGET(FOR_ITER): {
            //指令預測
            PREDICTED(FOR_ITER);
            /* before: [iter]; after: [iter, iter()] *or* [] */
            /* 從棧頂獲取iterator對象 */
            PyObject *iter = TOP();
            //調用迭代器類型對象的tp_iternext方法、傳入迭代器, 迭代出當前索引對應的元素, 然后索引+1, 然后下次迭代下一個元素
            PyObject *next = (*iter->ob_type->tp_iternext)(iter);
            //如果next不為NULL, 那么將元素壓入運行時棧, 顯然要賦值給for循環的變量了
            if (next != NULL) {
                PUSH(next);
                PREDICT(STORE_FAST);
                PREDICT(UNPACK_SEQUENCE);
                DISPATCH();
            }
            if (_PyErr_Occurred(tstate)) {
                //如果出現異常、並且沒有捕獲到, 那么報錯
                if (!_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) {
                    goto error;
                }
                //tstate指的是線程對象, 我們會后面分析, 這里與回溯棧相關
                else if (tstate->c_tracefunc != NULL) {
                    call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
                }
                _PyErr_Clear(tstate);
            }
            /* 走到這里說明本次迭代正常結束
            */
            STACK_SHRINK(1);
            Py_DECREF(iter);
            JUMPBY(oparg);
            PREDICT(POP_BLOCK);
            DISPATCH();
        }

FOR_ITER的指令代碼會首先從運行時棧中獲得PyListObject對象的迭代器,然后調用迭代器的tp_iternext開始進行迭代,迭代出元素的同時將索引+1。如果抵達了迭代器的結束位置,那么tp_iternext將返回NULL,這個結果預示着遍歷結束。

FOR_ITER的指令代碼會檢查tp_iternext的返回結果,如果得到的是一個有效的元素(next!=NULL),那么將獲得的這個元素壓入到運行時棧中,並開始進行一系列的字節碼預測動作。在我們當前的例子中,顯然會預測失敗,因此會執行STORE_NAME。那么如何獲取迭代器的下一個元素呢?

//listobject.c
static PyObject *
listiter_next(listiterobject *it)
{
    PyListObject *seq;
    PyObject *item;

    assert(it != NULL);
    //seq:顯然是獲取迭代器對象的PyListObject對象的指針
    seq = it->it_seq;
    if (seq == NULL)
        return NULL;
    //一定是一個PyListObject對象
    assert(PyList_Check(seq));
	
    //當前的索引小於列表的長度、即當前索引小於等於最大索引
    if (it->it_index < PyList_GET_SIZE(seq)) {
        //獲得索引為it_index的對應元素
        item = PyList_GET_ITEM(seq, it->it_index);
        //調整index, 使其自增1, 然后下一次遍歷得到下一個元素
        ++it->it_index;
        //增加引用計數、返回
        Py_INCREF(item);
        return item;
    }
	
    //迭代完畢之后,設置為NULL,所以迭代器只能夠順序迭代一次
    it->it_seq = NULL;
    Py_DECREF(seq);
    return NULL;
}

之后python虛擬機將沿着字節碼的順序一條一條的執行下去,從而完成輸出的動作。但是我們知道,for循環中肯定會有指令回退的動作,我們之前從字節碼中也看到了,for循環遍歷一次之后,會再次跳轉到FOR_ITER,而跳轉所使用的指令就是JUMP_ABSOLUTE

        case TARGET(JUMP_ABSOLUTE): {
            PREDICTED(JUMP_ABSOLUTE);
            //顯然這里的oparg表示字節碼偏移量, 表示直接跳轉到偏移量為oparg的位置上
            JUMPTO(oparg);
#if FAST_LOOPS
            FAST_DISPATCH();
#else
            DISPATCH();
#endif
        }

#define JUMPTO(x)       (next_instr = first_instr + (x) / sizeof(_Py_CODEUNIT))

可以看到和if不一樣,for循環使用的是絕對跳躍。JUMP_ABSOLUTE是強制設置next_instr的值,將next_instr設定到距離f->f_code->co_code開始地址的某一特定偏移的位置。這個偏移的量由JUMP_ABSOLUTE的指令參數決定,所以這條參數就成了for循環中指令回退動作的最關鍵的一點。

  2           0 LOAD_CONST               0 (1) 
              2 LOAD_CONST               1 (2) 
              4 BUILD_LIST               2     
              6 STORE_NAME               0 (lst) 

  3           8 LOAD_NAME                0 (lst) 
             10 GET_ITER					   
        >>   12 FOR_ITER                12 (to 26)
             14 STORE_NAME               1 (item) 

  4          16 LOAD_NAME                2 (print)
             18 LOAD_NAME                1 (item) 
             20 CALL_FUNCTION            1        
             22 POP_TOP						    
             24 JUMP_ABSOLUTE           12		 
        >>   26 LOAD_CONST               2 (None) 
             28 RETURN_VALUE

我們看到JUMP_ABSOLUTE的參數是12,next_str = 0 + 12 / 2 = 6,表示跳轉到字節碼偏移量為12、或者說第7條指令的位置上,也就是12 FOR_ITER這條指令,那么Python虛擬機的下一步動作就是執行FOR_ITER指令,即通過PyListObject對象的迭代器獲取PyListObject對象中的元素,然后依次向前,執行輸出,遇到JUMP_ABSOLUTE再跳轉回去。因此FOR_ITER指令和JUMP_ABSOLUTE指令之間構造出了一個循環結構,這個循環結構正是對應源碼中的for循環結構。

但是我們發現,FOR_ITER后面跟了一個參數,這里是12,可是目前為止我們並沒有看到有地方使用了這個12啊,那么它代表啥含義呢。其實,聰明如你肯定能猜到,因為從后面(to 26)也能看到,這是用於終止迭代的。表示從當前位置跳躍12個偏移量、等於24,或者在當前指令的基礎上再跳轉6條指令,也就是到達26 LOAD_CONST的位置。

終止迭代

"天下沒有不散的宴席",for循環也是要退出的,不用想這個退出的動作只能落在FOR_ITER的身上。在FOR_ITER指令執行的過程中,如果通過PyListObject對象的迭代器獲取的下一個元素不是有效的元素(會是NULL),這就意味着迭代結束了。這個結果將直接導致Python虛擬機會將迭代器對象從運行時棧中彈出,同時執行一個JUMPBY的動作,向前跳躍,在字節碼的層面上是向下,就是字節碼偏移量增大的方向。

#define JUMPBY(x)       (next_instr += (x) / sizeof(_Py_CODEUNIT))

        case TARGET(FOR_ITER): {
			
            /* 
            ...
            ...
            ...
            */
            
            //走到這里說明循環結束了
            STACK_SHRINK(1);
            Py_DECREF(iter);
            //直接進行跳轉
            JUMPBY(oparg);
            PREDICT(POP_BLOCK);
            DISPATCH();
        }

python虛擬機中的while循環控制結構

會了if、for,那么再來看while就簡單了。不僅如此,我們還要分析兩個關鍵字:break、continue,當然goto就別想了。

s = """
a = 0
while a < 10:
    a += 1
    if a == 5:
        continue
    if a == 7:
        break
    print(a)
"""


if __name__ == '__main__':
    import dis
    dis.dis(compile(s, "while", "exec"))

指令方面,while和for有很多是類似的。

  2           0 LOAD_CONST               0 (0)  //加載常量0
              2 STORE_NAME               0 (a)  //使用變量a存儲

  3     >>    4 LOAD_NAME                0 (a)  //進入while循環了, 首先是a < 10, 加載變量a
              6 LOAD_CONST               1 (10) //加載常量10
              8 COMPARE_OP               0 (<)  //比較操作
             10 POP_JUMP_IF_FALSE       50  //為False直接結束循環, 跳轉到字節碼偏移量為50的位置, 也就是第26條指令

  4          12 LOAD_NAME                0 (a) //這里是進入循環了, 加載變量a
             14 LOAD_CONST               2 (1) //加載常量1
             16 INPLACE_ADD 		       //執行a += 1操作, 這里相當於先執行了a + 1
             18 STORE_NAME               0 (a) //然后在重新讓變量a指向相加之后的結果

  5          20 LOAD_NAME                0 (a) //進入a == 5, 加載變量a
             22 LOAD_CONST               3 (5) //加載常量5
             24 COMPARE_OP               2 (==) //比較操作
             26 POP_JUMP_IF_FALSE       30  //如果為False, 那么直接跳轉到偏移量為30的位置, 也就是當前if語句的下一條指令

  6          28 JUMP_ABSOLUTE            4  //如果a == 5成立, 那么絕對跳轉, 跳到字節碼偏移量為4的位置, 所以continue對一個絕對跳轉, 目標是循環開始的地方

  7     >>   30 LOAD_NAME                0 (a) //走到這里說明a == 5不成立, 判斷a == 7, 加載變量a
             32 LOAD_CONST               4 (7) //加載常量7
             34 COMPARE_OP               2 (==) //比較是否相等
             36 POP_JUMP_IF_FALSE       40  //如果為False, 跳轉到偏移量為40的位置, 也就是print(a)

  8          38 JUMP_ABSOLUTE           50  //如果a == 5成立, 那么也是跳轉到字節碼偏移量為50的地方, 因為是break, 也是結束循環

  9     >>   40 LOAD_NAME                1 (print) //加載變量print
             42 LOAD_NAME                0 (a)  //加載變量a
             44 CALL_FUNCTION            1  //函數調用
             46 POP_TOP		            //從棧頂彈出返回值
             48 JUMP_ABSOLUTE            4  //走到這里說明while循環執行一圈了, 那么再度跳轉到while a < 10的地方
        >>   50 LOAD_CONST               5 (None)
             52 RETURN_VALUE

所以有了for循環,再看while循環就簡單多了,整體邏輯和for高度相似,當然里面還結合了if。另外我們看到break和continue都是使用了JUMP_ABSOLUTE實現的。JUMP_ABSOLUTE是跳轉到指定位置,通過絕對跳轉實現的。break是跳轉到while語句結束后的第一條指令;continue則是跳轉到while循環的開始位置。

然后執行一圈之后,遇到了48 JUMP_ABSOLUTE ,再度跳轉回去。當循環不滿足的時候,通過10 POP_JUMP_IF_FALSE 50直接結束循環,所以while事實上比for還是要簡單一些的。

Python虛擬機中的異常控制流

異常這個東西應該是最常見的了,程序在運行的過程中經常會遇到大量的錯誤,而Python中也定義了大量的異常類型供我們使用,下面我們來看看Python中的異常機制,因為這也是一個控制語句。

Python中的異常機制

Python虛擬機自身拋出異常

Python有一套內建的異常捕捉機制,即使在python的腳本文件中沒有出現try語句,python腳本執行出現的異常還是會被虛擬機捕捉到。首先我們就從ZeroDivisionError這個異常來分析。

s = """
1 / 0
"""


if __name__ == '__main__':
    import dis
    dis.dis(compile(s, "while", "exec"))
"""
  2           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (0)
              4 BINARY_TRUE_DIVIDE
              6 POP_TOP
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE
"""

我們看第3條字節碼指令,異常也正是在執行這條指令的時候觸發的。

        case TARGET(BINARY_TRUE_DIVIDE): {
            //co_consts -> (0, 1)
            PyObject *divisor = POP(); //1
            PyObject *dividend = TOP();//0
            //調用__truediv__
            PyObject *quotient = PyNumber_TrueDivide(dividend, divisor);
            Py_DECREF(dividend);
            Py_DECREF(divisor);
            //將結果設置在棧頂
            SET_TOP(quotient);
            //如果結果是NULL, 那么就報錯了
            if (quotient == NULL)
                goto error;
            DISPATCH();
        }

邏輯很簡單, 就是獲取兩個值,然后調用PyNumber_TrueDivide進行除法運算。正常情況下得到的肯定是一個數值,如果不能相除那么就返回NULL,如果接收的quotient是NULL,那么拋異常。因此我們來看看PyNumber_TrueDivide都干了些啥?

//longobject.c
//最終調用的是long_true_divide
//代碼很長我們截取一部分
static PyObject *
long_true_divide(PyObject *v, PyObject *w)
{	
    //都是在計算除法時需要的臨時變量
    PyLongObject *a, *b, *x;
    Py_ssize_t a_size, b_size, shift, extra_bits, diff, x_size, x_bits;
    digit mask, low;
    int inexact, negate, a_is_small, b_is_small;
    double dx, result;

    CHECK_BINOP(v, w);
    //將v和w中維護的整數值轉存到a和b中
    a = (PyLongObject *)v;
    b = (PyLongObject *)w;
    a_size = Py_ABS(Py_SIZE(a));
    b_size = Py_ABS(Py_SIZE(b));
    negate = (Py_SIZE(a) < 0) ^ (Py_SIZE(b) < 0);
    //獲取b_size, 就是b對應的ob_size, 我們在分析PyLongObject對象時說過, 如果這個對象維護的值為0,那么ob_size就是0,這是個特殊情況
    //並且這個ob_size還可以體現出維護的值的正負
    //我們看到如果b_size == 0, 那么拋出PyExc_ZeroDivisionError
    if (b_size == 0) {
        PyErr_SetString(PyExc_ZeroDivisionError,
                        "division by zero");
        goto error;
    }
    ...
    ...    
}

所以如果除以0,那么直接設置異常信息。另外我們說過Python中一切皆對象,那么異常也是一個對象,是一個PyObject類型。

//pyerrors.h
//這里面定義了大量的異常, 比如:

typedef struct {
    PyException_HEAD
} PyBaseExceptionObject; //BaseException, 所有異常的基類, Exception也繼承自它


typedef struct {
    PyException_HEAD
    PyObject *msg;
    PyObject *filename;
    PyObject *lineno;
    PyObject *offset;
    PyObject *text;
    PyObject *print_file_and_line;
} PySyntaxErrorObject; //語法異常


typedef struct {
    PyException_HEAD
    PyObject *msg;
    PyObject *name;
    PyObject *path;
} PyImportErrorObject; //導包異常


typedef struct {
    PyException_HEAD
    PyObject *encoding;
    PyObject *object;
    Py_ssize_t start;
    Py_ssize_t end;
    PyObject *reason;
} PyUnicodeErrorObject;//Unicode異常


typedef struct {
    PyException_HEAD
    PyObject *value;
} PyStopIterationObject; //StopIteration異常

在線程狀態對象中記錄異常信息(線程的知識后續會說)

我們之前看到,異常信息是通過PyErr_SetString(異常類型, 異常信息)來設置的,而除了這個PyErr_SetString,還會經過PyErr_SetObject,最終到達PyErr_Restore。在PyErr_Restore中,Python將這個異常放置到了一個安全的地方。

//Python/errors.c

void
PyErr_Restore(PyObject *type, PyObject *value, PyObject *traceback)
{	
    //獲取線程對象
    PyThreadState *tstate = _PyThreadState_GET();
    _PyErr_Restore(tstate, type, value, traceback);
}


void
_PyErr_Restore(PyThreadState *tstate, PyObject *type, PyObject *value,
               PyObject *traceback)
{	
    //異常類型、異常值、異常的回溯棧, 對應Python中sys.exc_info()返回的元組里面的3個元組
    PyObject *oldtype, *oldvalue, *oldtraceback;
	
    //如果traceback不為空並且不是回溯棧, 那么將其設置為NULL
    if (traceback != NULL && !PyTraceBack_Check(traceback)) {
        Py_DECREF(traceback);
        traceback = NULL;
    }

    //獲取以前的異常信息
    oldtype = tstate->curexc_type;
    oldvalue = tstate->curexc_value;
    oldtraceback = tstate->curexc_traceback;
	
    //設置當前的異常信息
    tstate->curexc_type = type;
    tstate->curexc_value = value;
    tstate->curexc_traceback = traceback;
	
    //將之前的異常信息的引用計數分別減1
    Py_XDECREF(oldtype);
    Py_XDECREF(oldvalue);
    Py_XDECREF(oldtraceback);
}

最后在tstate(PyThreadState對象)的curexc_type中存下了PyExc_ZeroDivisionError,而cur_value中存下了字符串division by zero,curexc_traceback存下了回溯棧。

import sys

try:
    1 / 0
except ZeroDivisionError as e:
    exc_type, exc_value, exc_tb = sys.exc_info()
    print(exc_type)  # <class 'ZeroDivisionError'>
    print(exc_value)  # division by zero
    print(exc_tb)  # <traceback object at 0x000001C43F29F4C0>

    # exc_tb也可以通過e.__traceback__獲取
    print(e.__traceback__ is exc_tb)  # True

我們再來看看PyThreadState對象(這里先簡單看一下,后續會詳細說),這個之前說了是與線程有關的,但是它只是線程信息的一個抽象描述,而真實的線程及狀態肯定是由操作系統來維護和管理的。因為Python虛擬機在運行的時候總需要另外一些與線程相關的狀態和信息,比如是否發生了異常等等,這些信息顯然操作系統是沒有辦法提供的。而PyThreadState對象正是Python為線程准備的、在虛擬機層面保存線程狀態信息的對象(后面簡稱線程狀態對象、或者線程對象)。在這里,當前活動線程(OS原生線程)對應的PyThreadState對象可以通過PyThreadState_GET獲得,在得到了線程狀態對象之后,就將異常信息存放到線程狀態對象中。

展開棧幀

首先我們知道異常已經被記錄在了線程的狀態中了,現在可以回頭看看,在跳出了分派字節碼指令的switch塊所在的for循環之后,發生了什么動作。

我們知道在Python/ceval.c中有一個 _PyEval_EvalFrameDefault 函數,它是執行字節碼指令的。里面有一個for循環,會依次遍歷每一條字節碼,在這個for循環里面有一個巨型switch,里面case了所有指令出現的情況。當所有指令執行完畢之后,這個for循環就結束了。

但這里還存在一個問題,那就是導致跳出那個巨大的switch塊所在的for循環的原因:"1. 可以是執行完了所有的字節碼之后正常跳出","2. 也可以是發生異常后跳出",那么Python虛擬機到底如何區分這是哪一種呢?

PyObject* _Py_HOT_FUNCTION 
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
    for (;;) {
        switch (opcode) {
            // 一個超大的switch語句
        }
		
error: //一旦出現異常, 會使用goto語句跳轉到error標簽這里
#ifdef NDEBUG
        if (!_PyErr_Occurred(tstate)) {
            _PyErr_SetString(tstate, PyExc_SystemError,
                             "error return without exception set");
        }
#else
        assert(_PyErr_Occurred(tstate));
#endif

        //創建traceback對象
        PyTraceBack_Here(f);

        if (tstate->c_tracefunc != NULL)
            call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj,
                           tstate, f);

    }
}

如果在執行switch語句的時候出現了異常,那么會跳轉到error這里,否則會跳轉到其它地方。當跳轉到error標簽的時候就代表出現異常了,注意:是在執行過程中出現異常之后Python虛擬機才獲取到異常信息。

那么問題就來了, 如果在在涉及到函數調用的時候發生了異常該怎么辦呢?首先在python虛擬機意識到有異常發生后,它就要開始進入異常處理的流程,這個流程會涉及到我們介紹PyFrameObject對象時所提到的那個PyFrameObject對象鏈表。在介紹PyFrameObject對象的時候,我們說過PyFrameObject實際上就是對棧幀的模擬,當發生函數函數調用,python會新創建一個棧幀,並將其內部的f_back連接到調用者對應的PyFrameObject,這樣就形成了一條棧幀鏈。

def h():
    1 / 0

def g():
    h()

def f():
    g()
    
f()
"""
Traceback (most recent call last):
  File "D:/satori/1.py", line 13, in <module>
    f()
  File "D:/satori/1.py", line 10, in f
    g()
  File "D:/satori/1.py", line 6, in g
    h()
  File "D:/satori/1.py", line 2, in h
    1 / 0
ZeroDivisionError: division by zero
"""

這是腳本運行時產生的輸出,我們看到了函數調用的信息:比如在源代碼的哪一行調用了哪一個函數,那么這些信息是從何而來的呢?而且我們發現輸出的信息是一個鏈狀的結構,是不是和棧幀鏈比較相似啊。沒錯,在Python虛擬機處理異常的時候,涉及到了一個traceback對象,在這個對象中記錄棧幀鏈表的信息,Python虛擬機利用這個對象來將棧幀鏈表中的每一個棧幀的狀態進行可視化,這個可視化的結果就是上面輸出的異常信息。

回到我們的例子,當異常發生時,當前活動的棧幀是函數h對應的棧幀。在Python虛擬機開始處理異常的時候,它首先的動作就是創建一個traceback對象,用於記錄異常發生時活動棧幀的狀態。

PyObject* _Py_HOT_FUNCTION 
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{
    for (;;) {
        switch (opcode) {
            // 一個超大的switch語句
        }
		
        //......
        
        //創建traceback對象
        PyTraceBack_Here(f);
		
        //這里tstate還是我們之前提到的與當前活動線程對應的線程對象
        //其中的c_tracefunc是用戶自定義的追蹤函數,主要用於編寫python的debugger。
        //但是通常情況下這個值都是NULL,所以不考慮它。
        //我們主要看上面的PyTraceBack_Here(f),它到底使用PyFrameObject對象創建了一個怎樣的traceback
        if (tstate->c_tracefunc != NULL)
            call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj,
                           tstate, f);

    }
}


//Python/traceback.c
int
PyTraceBack_Here(PyFrameObject *frame)
{
    PyObject *exc, *val, *tb, *newtb;
    //獲取線程中保存線程狀態的traceback對象, 進行設置
    PyErr_Fetch(&exc, &val, &tb);
    //_PyTraceBack_FromFrame創建新的traceback對象
    newtb = _PyTraceBack_FromFrame(tb, frame);
    if (newtb == NULL) {
        _PyErr_ChainExceptions(exc, val, tb);
        return -1;
    }
    //將新的traceback對象交給線程狀態對象
    PyErr_Restore(exc, val, newtb);
    Py_XDECREF(tb);
    return 0;
}

原來traceback對象是保存在線程狀態對象之中的,我們來看看這個traceback對象究竟長得什么樣:

//Include/cpython/traceback.h
typedef struct _traceback {
    PyObject_HEAD
    struct _traceback *tb_next;
    struct _frame *tb_frame;
    int tb_lasti;
    int tb_lineno;
} PyTracebackObject;

可以看到里面有一個tb_next,所以很容易想到這個traceback也是一個鏈表結構。其實這個PyTracebackObject對象的鏈表結構應該跟PyFrameObject對象的鏈表結構是同構的、或者說一一對應的,即一個PyFrameObject對象應該對應一個PyTracebackObject對象。我們看看這個鏈表是怎么產生的,在PyTraceBack_Here函數中我們看到它是通過_PyTraceBack_FromFrame創建的,那么秘密就隱藏在這個函數中:

//Python/traceback.h
PyObject*
_PyTraceBack_FromFrame(PyObject *tb_next, PyFrameObject *frame)
{
    assert(tb_next == NULL || PyTraceBack_Check(tb_next));
    assert(frame != NULL);
	
    //底層調用了tb_create_raw, 參數分別是下一個traceback、當前棧幀、當前f_lasti、以及源代碼行號
    return tb_create_raw((PyTracebackObject *)tb_next, frame, frame->f_lasti,
                         PyFrame_GetLineNumber(frame));
}

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_next就是下一個traceback
        tb->tb_next = next;
        Py_XINCREF(frame);
        //設置棧幀, 所以我們可以通過e.__traceback__.tb_frame獲取棧幀
        tb->tb_frame = frame;
        //執行完畢時字節碼偏移量
        tb->tb_lasti = lasti;
        //源代碼行號
        tb->tb_lineno = lineno;
        //加入GC追蹤, 參與垃圾回收
        PyObject_GC_Track(tb);
    }
    return (PyObject *)tb;
}

從源碼中我們看到,tb_next是將兩個traceback連接了起來,不過這個和PyFrameObject里面f_back正好相反。f_back指向的是上一個棧幀,而tb_next指向的是下一個traceback。另外在新創建的對象中,還使用tb_frame和對應的PyFrameObject對象建立了聯系,當然還有最后執行完畢的字節碼偏移量以及其在源代碼中對應的行號。話說還記得PyCodeObject對象中的那個co_lnotab嗎,這里的tb_lineno就是通過co_lnotab獲取的。

Python虛擬機意識到有異常拋出,並創建了traceback對象之后,它會在當前棧幀中尋找except語句,來執行開發人員指定的捕捉異常的動作。如果沒有找到,那么Python虛擬機將退出當前的活動棧幀,並沿着棧幀鏈回退到上一個棧幀,在上一個棧幀中尋找except語句。就像我們之前說的,出現函數調用會創建棧幀,當函數執行完畢或者出現異常的時候,會回退到上一級棧幀。一層一層創建、一層一層返回。至於回退的這個動作,則是在PyEval_EvalFrameEx的最后完成,當然准確的說應該是其內部調用的_PyEval_EvalFrameDefault的最后。

    for(;;){
        switch(opcode){
            //巨型switch
        }
exception_unwind:
    //如果發生了異常, 這里會將異常進行展開, 然后試圖進行捕獲
    //注意: exception_unwind是位於這個大大的for循環的內部的結束位置
        while (f->f_iblock > 0) {
            //里面是和異常捕獲相關的邏輯, 后面會分析
        }
        break;    
    }
	
    //retval表示_PyEval_EvalFrameDefault函數的返回值, 返回值為NULL, 那么表示有異常發生	
    assert(retval == NULL);
    assert(_PyErr_Occurred(tstate));
    //......
exit_eval_frame:
    if (PyDTrace_FUNCTION_RETURN_ENABLED())
        dtrace_function_return(f);
    Py_LeaveRecursiveCall();
    f->f_executing = 0;
    //將線程狀態對象中的活動棧幀設置為上一個棧幀, 完成棧幀回退的動作
    tstate->frame = f->f_back;
    return _Py_CheckFunctionResult(NULL, retval, "PyEval_EvalFrameEx");

如果開發人員沒有任何的捕獲異常的動作,那么將通過break跳出python執行字節碼的那個for循環。最后,由於沒有捕獲到異常, 其返回值被設置為NULL,同時通過將當前線程狀態對象中的活動棧幀,設置為上一級棧幀,從而完成棧幀回退的動作。

此時我們的例子就很好解釋了,當虛擬機執行函數f時,它是在PyEval_EvalFrameEx(內部調用的_PyEval_EvalFrameDefault)中執行與f對應的PyFrameObject對象中的字節碼指令序列。當在函數f中調用g時,Python虛擬機又會為函數g創建新的PyFrameObject對象,會把控制權交給函數g對應的PyFrameObject,當然調用的也是PyEval_EvalFrameEx,只不過這次是在執行與g對應的PyFrameObject對象中的字節碼指令序列了。同理函數g調用函數h的時候,也是一樣的。所以當在函數h中發生異常,沒有異常捕獲、導致PyEval_EvalFrameEx結束時,自然要返回到、或者把控制權再交給與函數g對應的PyFrameObject,由PyEval_EvalFrameEx繼續執行。由於在返回時,retval被設置為NULL,所以回到g中,Python虛擬機再次意識到有異常產生,可由於函數g中調用的時候也沒有異常捕獲,那么同樣也要退出,再把PyEval_EvalFrameEx執行棧幀的控制權交給函數f對應的棧幀,如果還沒有異常捕獲,那么回到py文件對應的棧幀,再沒有的話就直接報錯了。

這個沿着棧幀鏈不斷回退的過程我們稱之為棧幀展開,在這個棧幀展開的過程中,Python虛擬機不斷地創建與各個棧幀對應的traceback,並將其鏈接成鏈表。

由於我們沒有設置任何的異常捕獲的代碼,那么python虛擬機的執行流程會一直返回到PyRun_SimpleFileExFlags中,這個PyRun_SimpleFileExFlags是干啥的我們先不管,以后分析Python運行時候的初始化時,就可以看到這個函數的作用了。

//Python/pythonrun.c
int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
                        PyCompilerFlags *flags)
{	
    //.......
    if (maybe_pyc_file(fp, filename, ext, closeit)) {
       //......
       //執行pyc文件 
    } else {
        /* When running from stdin, leave __main__.__loader__ alone */
        if (strcmp(filename, "<stdin>") != 0 &&
            set_main_loader(d, filename, "SourceFileLoader") < 0) {
            fprintf(stderr, "python: failed to set __main__.__loader__\n");
            ret = -1;
            goto done;
        }
        //調用了PyRun_FileExFlags
        v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
                              closeit, flags);
    }
    //......
    return ret;
}


PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
                  PyObject *locals, int closeit, PyCompilerFlags *flags)
{
    //......
    //調用了run_mod
    ret = run_mod(mod, filename, globals, locals, flags, arena);

exit:
    Py_XDECREF(filename);
    if (arena != NULL)
        PyArena_Free(arena);
    return ret;
}


static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
            PyCompilerFlags *flags, PyArena *arena)
{
    //......
    //調用run_eval_code_obj
    v = run_eval_code_obj(co, globals, locals);
    Py_DECREF(co);
    return v;
}


static PyObject *
run_eval_code_obj(PyCodeObject *co, PyObject *globals, PyObject *locals)
{
    //......
    //調用了PyEval_EvalCode
    v = PyEval_EvalCode((PyObject*)co, globals, locals);
    if (!v && PyErr_Occurred() == PyExc_KeyboardInterrupt) {
        _Py_UnhandledKeyboardInterrupt = 1;
    }
    return v;
}


//Python/ceval.c
PyObject *
PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals)
{	
    //調用了PyEval_EvalCodeEx
    return PyEval_EvalCodeEx(co,
                      globals, locals,
                      (PyObject **)NULL, 0,
                      (PyObject **)NULL, 0,
                      (PyObject **)NULL, 0,
                      NULL, NULL);
}


PyObject *
PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
                  PyObject *const *args, int argcount,
                  PyObject *const *kws, int kwcount,
                  PyObject *const *defs, int defcount,
                  PyObject *kwdefs, PyObject *closure)
{	
    //調用了_PyEval_EvalCodeWithName
    return _PyEval_EvalCodeWithName(_co, globals, locals,
                                    args, argcount,
                                    kws, kws != NULL ? kws + 1 : NULL,
                                    kwcount, 2,
                                    defs, defcount,
                                    kwdefs, closure,
                                    NULL, NULL);
}


PyObject *
_PyEval_EvalCodeWithName(PyObject *_co, PyObject *globals, PyObject *locals,
           PyObject *const *args, Py_ssize_t argcount,
           PyObject *const *kwnames, PyObject *const *kwargs,
           Py_ssize_t kwcount, int kwstep,
           PyObject *const *defs, Py_ssize_t defcount,
           PyObject *kwdefs, PyObject *closure,
           PyObject *name, PyObject *qualname)
{
    //......
    //調用了PyEval_EvalFrameEx, 里面的f就是在該函數中創建的棧幀對象
    //還記得這個返回值retval嗎? 如果它是NULL, 那么代表該棧幀中有異常發生了
    retval = PyEval_EvalFrameEx(f,0);

fail: 
    //......
    //返回retval
    return retval;
}


PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{	
    //創建一個線程對象, 里面會調用其它函數創建一個線程
    PyInterpreterState *interp = _PyInterpreterState_GET_UNSAFE();
    //執行線程對象的eval_frame
    return interp->eval_frame(f, throwflag);
}


//Python/pystate.c
PyInterpreterState *
PyInterpreterState_New(void)
{
    //......
    //你看到了什么? interp->eval_frame被設置成了_PyEval_EvalFrameDefault
    interp->eval_frame = _PyEval_EvalFrameDefault;
    //......
}

可以看到兜了這么多圈,最終PyRun_SimpleFileExFlags返回的值就是PyEval_EvalFrameEx返回的那個retval(當然出現異常的話,就是NULL)。所以接下來會調用PyErr_Print,然后在PyErr_Print中,Python虛擬機取出其維護的traceback,並遍歷traceback鏈表,逐個輸出其中的信息,也就是我們在python中看到的那個打印的異常信息。並且這個順序是:.py文件、函數f、函數g、函數h,不是函數h、函數g、函數f、py文件。因為每一個棧幀對應一個traceback,而且是按照順序遍歷的,所以是:.py文件、函數f、g、h的順序,當然從打印這一點也能看出來。

因為是在函數h中報的錯,所以退到函數g的棧幀中尋找異常捕獲;如果retval為NULL,那么在退到函數f的棧幀中尋找異常捕獲,再沒有的話則退到模塊對應的棧幀中。

模塊中也沒有異常捕獲,那么報錯。所以獲取模塊棧幀對應的traceback,打印異常信息,然后通過tb_next找到 f 對應的traceback打印其信息,依次下去......。事實上稍微想一下就能理解,雖然是在 h 中報的錯,但根本原因是我們在模塊中調用了 f,所以依次打印模塊、f、g、h中traceback的異常信息。

Python中的異常捕獲

目前我們知道了Python中的異常在虛擬機級別是什么,拋出異常這個動作在虛擬機層面上是怎樣的一個行為,最后我們還知道了Python在處理異常時候的棧幀展開行為。但這只是Python虛擬機中內建的處理異常的動作,並沒有使用Python語言中提供的異常捕獲,下面我們就來看一下Python提供的異常捕獲機制是如何影響Python虛擬機的異常處理流程的。

s = """
try:
    raise Exception("raise an exception")
except Exception as e:
    print(e)
finally:
    print("finally code")
"""


if __name__ == '__main__':
    import dis
    dis.dis(compile(s, "exception", "exec"))
  2           0 SETUP_FINALLY           60 (to 62)
              2 SETUP_FINALLY           12 (to 16)

  3           4 LOAD_NAME                1 (Exception)
              6 LOAD_CONST               1 ('raise an exception')
              8 CALL_FUNCTION            1
             10 RAISE_VARARGS            1
             12 POP_BLOCK
             14 JUMP_FORWARD            42 (to 58)

  4     >>   16 DUP_TOP
             18 LOAD_NAME                1 (Exception)
             20 COMPARE_OP              10 (exception match)
             22 POP_JUMP_IF_FALSE       56
             24 POP_TOP
             26 STORE_NAME               2 (e)
             28 POP_TOP
             30 SETUP_FINALLY           12 (to 44)

  5          32 LOAD_NAME                0 (print)
             34 LOAD_NAME                2 (e)
             36 CALL_FUNCTION            1
             38 POP_TOP
             40 POP_BLOCK
             42 BEGIN_FINALLY
        >>   44 LOAD_CONST               2 (None)
             46 STORE_NAME               2 (e)
             48 DELETE_NAME              2 (e)
             50 END_FINALLY
             52 POP_EXCEPT
             54 JUMP_FORWARD             2 (to 58)
        >>   56 END_FINALLY
        >>   58 POP_BLOCK
             60 BEGIN_FINALLY

  7     >>   62 LOAD_NAME                0 (print)
             64 LOAD_CONST               0 ('finally code')
             66 CALL_FUNCTION            1
             68 POP_TOP
             70 END_FINALLY
             72 LOAD_CONST               2 (None)
             74 RETURN_VALUE

首先這個指令集比較復雜,因為要分好幾種情況。try里面沒有出現異常;try里面出現了異常、但是except語句沒有捕獲到;try里面出現了異常,except語句捕獲到了。但我們知道無論是哪種情況,都要執行finally。

我們先看上面的SETUP_FINALLY指令,這里為包含finally語句做准備的:

        case TARGET(SETUP_FINALLY): {
            /* NOTE: If you add any new block-setup opcodes that
               are not try/except/finally handlers, you may need
               to update the PyGen_NeedsFinalizing() function.
               */
            //我們看到僅僅是調用了一個PyFrame_BlockSetup函數
            PyFrame_BlockSetup(f, SETUP_FINALLY, INSTR_OFFSET() + oparg,
                               STACK_LEVEL());
            DISPATCH();
        }

//Objects/frameobject.c
void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{	
    //創建一個PyTryBlock *
    PyTryBlock *b;
    //這個f_iblock為當前指令在f_blockstack上的索引, 還記得這個f_blockstack嗎?我們在介紹棧幀的時候說過的,它可以用於try代碼塊
    //f_blockstack是一個數組, 內部存儲了多個PyTryBlock對象
    //PyTryBlock f_blockstack[CO_MAXBLOCKS]; CO_MAXBLOCKS是一個宏,為20
    if (f->f_iblock >= CO_MAXBLOCKS)
        Py_FatalError("XXX block stack overflow");
    //這里我們算是真正意義上第一次使用棧幀中的f_blockstack屬性
    //這里得到的b顯然是個PyTryBlock結構體實例
    b = &f->f_blockstack[f->f_iblock++];
    //設置屬性
    b->b_type = type;
    b->b_level = level;
    b->b_handler = handler;
}


//frameobject.h
//我們看看PyTryBlock長什么樣
typedef struct {
    int b_type;                 /* what kind of block this is */
    int b_handler;              /* where to jump to find handler */
    int b_level;                /* value stack level to pop to */
} PyTryBlock;
//顯然PyFrameObject對象中的f_blockstack是一個由PyTryBlock對象組成的數組,而SETUP_FINALLY指令所做的就是從這個數組中獲得了一塊PyTryBlock對象
//並在這個對象中存放了一些Python虛擬機當前的狀態信息。比如當前執行的字節碼指令,當前運行時棧的深度等等。
//那么這個結構在try控制結構中起着什么樣的作用呢?我們后面就會知曉
//我們注意到PyTryBlock中有一個b_type域,注釋寫着這個域是用來表示是block的種類, 也就意味着存在着多種不同用途的PyTryBlock對象。
//從PyFrame_BlockSetup中可以看到,這個b_type實際上被設置為當前Python虛擬機正在執行的字節碼指令,以字節碼指令作為區分PyTryBlock的不同用途

但我們看到開頭有兩個SETUP_FINALLY,其實在Python3.8之前,第二個SETUP_FINALLY應該是SETUP_EXCEPT,但是在3.8中都變成了SETUP_FINALLY。

在這里分出兩塊PyTryBlock,肯定是要在捕捉異常的時候用。不過別着急,我們先回到拋出異常的地方看看:10 RAISE_VARARGS 1。在RAISE_VARARGS之前,通過LOAD_NAMELOAD_CONSTCALL_FUNCTION構造出了一個異常對象,當然盡管Exception是一個類,但調用的指令也同樣是CALL_FUNCTION(至於這個指令的剖析和對象的創建后面章節會介紹,這里只需要知道一個異常已經被創建出來了),並將這個異常壓入棧中。而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;
        }

這里RAISE_VARARGS后面的參數是1,所以直接將異常對象取出賦給exc,然后調用do_raise函數。在do_raise中,最終調用之前的說過的PyErr_Restore函數,將異常對象存儲到當前的線程對象中。在經過了一系列繁復的動作之后(比如創建並設置traceback),通過do_raise,Python虛擬機將攜帶着(f_iblock=2)信息抵達真正捕捉異常的代碼,我們看到跳轉到了標簽為exception_unwind的地方進行異常捕獲,並且在最后,Python虛擬機通過一個break的動作跳出了分發字節碼指令的那個巨大的switch語句所在的for循環。

exception_unwind:
        /* Unwind stacks if an exception occurred */
        while (f->f_iblock > 0) {
            PyTryBlock *b = &f->f_blockstack[--f->f_iblock];
            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);
                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);
                /* Resume normal execution */
                goto main_loop;
            }
        } /* unwind stack */

        /* End the loop as we still have an error */
        break;
    } /* main loop */

    assert(retval == NULL);
    assert(_PyErr_Occurred(tstate));

Python虛擬機首先從當前的PyFrameObject對象中的f_blockstack中彈出一個PyTryBlock來,從代碼中能看到彈出的是b_type = SETUP_FINALLY, b_handler=16的PyTryBlock。另一方面,Python虛擬機通過PyErr_Fetch得到了當前線程狀態對象中存儲的最新的異常對象和traceback對象:

//Python/errors.c

void
PyErr_Fetch(PyObject **p_type, PyObject **p_value, PyObject **p_traceback)
{
    PyThreadState *tstate = _PyThreadState_GET();
    _PyErr_Fetch(tstate, p_type, p_value, p_traceback);
}

void
_PyErr_Fetch(PyThreadState *tstate, PyObject **p_type, PyObject **p_value,
             PyObject **p_traceback)
{
    *p_type = tstate->curexc_type;
    *p_value = tstate->curexc_value;
    *p_traceback = tstate->curexc_traceback;

    tstate->curexc_type = NULL;
    tstate->curexc_value = NULL;
    tstate->curexc_traceback = NULL;
}

回到exception_unwind,我們看到之后python虛擬機調用PUSH將tb、val、exc分別壓入運行時棧中,而且Python知道此時程序猿已經為異常處理做好了准備,所以接下來的異常處理工作,則需要交給程序員指定的代碼來解決,這個動作通過JUMP_FORWARD(JUMPTO(b->b_handler))來完成。JUMPTO其實僅僅是進行了一下指令的跳躍,將Python虛擬機將要執行的下一條指令設置為異常處理代碼編譯后所得到的第一條字節碼指令。

因為f_blockstack是從后往前彈出的,所以第一個彈出的是PyTryBlock中b_handler為16的SETUP_FINALLY,那么Python虛擬機將要執行的下一條指令就是偏移量為16的那條指令,而這條指令就是DUP_TOP,異常處理代碼對應的第一條字節碼指令。

        case TARGET(DUP_TOP): {
            PyObject *top = TOP();
            Py_INCREF(top);
            PUSH(top);
            FAST_DISPATCH();
        }

首先我們except Exception,毫無疑問要LOAD_NAME,把這個異常給load進來,然后調用指令COMPARE_OP,這個顯然就是比較我們指定捕獲的異常和運行時棧中存在的那個被捕獲的異常是否匹配。POP_JUMP_IF_FALSE如果為Py_True表示匹配,那么繼續往下執行print(e)對應的字節碼指令,POP_TOP將異常從棧頂彈出,賦值給e,然后打印等等。如果POP_JUMP_IF_FALSE為Py_False表示不匹配,那么我們發現直接跳轉到了56 END_FINALLY,因為異常不匹配的話,那么異常的相關信息還是要重新放回線程對象當中,讓Python重新引發異常,而這個動作就由END_FINALLY完成,通過PyErr_Restore函數將異常信息重新寫回線程對象中。

        case TARGET(END_FINALLY): {
            PREDICTED(END_FINALLY);
            /* At the top of the stack are 1 or 6 values:
               Either:
                - TOP = NULL or an integer
               or:
                - (TOP, SECOND, THIRD) = exc_info()
                - (FOURTH, FITH, SIXTH) = previous exception for EXCEPT_HANDLER
            */
            PyObject *exc = POP();
            if (exc == NULL) {
                FAST_DISPATCH();
            }
            else if (PyLong_CheckExact(exc)) {
                int ret = _PyLong_AsInt(exc);
                Py_DECREF(exc);
                if (ret == -1 && _PyErr_Occurred(tstate)) {
                    goto error;
                }
                JUMPTO(ret);
                FAST_DISPATCH();
            }
            else {
                assert(PyExceptionClass_Check(exc));
                PyObject *val = POP();
                PyObject *tb = POP();
                //將異常信息又寫入到了線程狀態對象當中了
                _PyErr_Restore(tstate, exc, val, tb);
                goto exception_unwind;
            }
        }

然而不管異常是否匹配,最終處理異常的兩條岔路都會在58 POP_BLOCK處匯合。

        PREDICTED(POP_BLOCK);
        TARGET(POP_BLOCK) {
            //這里將當前PyFrameObject的f_blockstack中還剩下的那個與SETUP_FINALLY對應的PyTryBlock對象彈出
            //然后python虛擬機的流程就進入了與finally表達式對應的字節碼指令了。
            PyTryBlock *b = PyFrame_BlockPop(f);
            UNWIND_BLOCK(b);
            DISPATCH();
        }

因此在Python異常機制的實現中,最終要的就是虛擬機狀態以及PyFrameObject對象中f_blockstack里存放的PyTryBlock對象了。首先根據Python虛擬機狀態可以判斷當前是否發生了異常,而PyTryBlock對象則告訴python虛擬機,程序員是否為異常設置了except代碼塊和finally代碼塊,python虛擬機異常處理的流程就是在虛擬機所處的狀態和PyTryBlock的共同作用下完成的。

還是那句話,在3.8之前Python的指令集中存在一個SETUP_EXCEPT,但是在3.8的時候只有SETUP_FINALLY了。

總之Python中一旦出現異常了,那么會將異常類型、異常值、異常回溯棧設置在線程狀態對象中,然后棧幀一步一步的后退尋找異常捕獲代碼(從內向外)。如果退到了模塊級別還沒有發現異常捕獲,那么從外向內打印traceback中的信息,當走到最后一層的時候再將線程中設置的異常類型和異常值打印出來。

def h():
    1 / 0

def g():
    h()

def f():
    g()
    
f()

# 首先是在模塊中調用了f、f調用了g、g調用了h, 所以在h中出現了異常、發現又沒有異常捕獲, 所以將執行權交給函數g對應的棧幀
# 但是g也沒有異常捕獲, 所以再將執行權交給函數f對應的棧幀, 所以調用的時候棧幀一層一層創建, 執行完畢、或者出現異常, 棧幀一層一層向后退
# 所以h的f_back指向g、g的f_back指向f、f的f_back指向模塊、模塊的f_back為None
# 但是對應的traceback則是模塊的tb_next指向f、f的tb_next指向g、g的tb_next指向h、h的tb_next為None
# 而我們說棧幀層層后退, 退到模塊對應的棧幀的時候要是還沒有發現異常捕獲, 那么就報錯了
# 所以此時會打印模塊對應的traceback的信息, 然后依次是f、g、h, 因為棧幀是從"函數h到模塊"、但traceback則是從"模塊到函數h"
# 所以我們仔細觀察一下輸出的異常信息, 不難印證我們的結論
"""
Traceback (most recent call last):  # traceback回溯棧
  File "D:/satori/1.py", line 13, in <module>  # 打印模塊的traceback
    f()
  File "D:/satori/1.py", line 10, in f  # 打印f的traceback
    g()
  File "D:/satori/1.py", line 6, in g   # 打印g的traceback
    h()
  File "D:/satori/1.py", line 2, in h   # 打印h的traceback
    1 / 0
ZeroDivisionError: division by zero  # h的tb_next為None, 證明是在h中發生了錯誤, 所以再將之前設置線程狀態對象中異常類型和異常值打印出來即可
"""

至於Python在處理異常的時候都經歷哪些歷程,我們雖然分析了,但其實還不夠詳細。因為Python的異常機制牽扯到底層的方方面面,並且涉及到了很多的宏,有興趣可以自己再仔細深入研究。另外需要注意的是:Python3.8變化還是比較大的,在字節碼方面你通過和3.7對比就可以發現。

最后再看一個思考題

e = 2.718
try:
    raise Exception("我要引發異常了")
except Exception as e:
    print(e)  # 我要引發異常了

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

why?我們發現在外面打印e的時候,告訴我們e沒有被定義。這是為什么呢?首先可以肯定的是,肯定是except Exception as e導致的,因為我們as的也是e,和外面的e重名了,如果我們as的是e1呢?

e = 2.718
try:
    raise Exception("我要引發異常了")
except Exception as e1:
    print(e1)  # 我要引發異常了

print(e)  # 2.718

可以看到as的是e1就沒有問題了,但是為什么呢?即便不知道原因,也能推測出來。因為外面的變量叫e,而我們捕獲異常as的也是e,此時e的指向就變了,而當異常處理結束的時候,e這個變量就被銷毀了,所以外面就找不到了。然而事實上也確實如此。我們可以看一下字節碼,通過觀察我們上面例子的字節碼,就能很清晰地看出端倪了。

  1           0 LOAD_CONST               0 (2.718)
              2 STORE_NAME               0 (e)

  2           4 SETUP_FINALLY           12 (to 18)

  3           6 LOAD_NAME                1 (Exception)
              8 LOAD_CONST               1 ('我要引發異常了')
             10 CALL_FUNCTION            1
             12 RAISE_VARARGS            1
             14 POP_BLOCK
             16 JUMP_FORWARD            42 (to 60)

  4     >>   18 DUP_TOP
             20 LOAD_NAME                1 (Exception)
             22 COMPARE_OP              10 (exception match)
             24 POP_JUMP_IF_FALSE       58
             26 POP_TOP
             28 STORE_NAME               0 (e)
             30 POP_TOP
             32 SETUP_FINALLY           12 (to 46)

  5          34 LOAD_NAME                2 (print)
             36 LOAD_NAME                0 (e)
             38 CALL_FUNCTION            1
             40 POP_TOP
             42 POP_BLOCK
             44 BEGIN_FINALLY
        >>   46 LOAD_CONST               2 (None)
             48 STORE_NAME               0 (e)
             50 DELETE_NAME              0 (e)
             52 END_FINALLY
             54 POP_EXCEPT
             56 JUMP_FORWARD             2 (to 60)
        >>   58 END_FINALLY
        >>   60 LOAD_CONST               2 (None)
             62 RETURN_VALUE

字節碼很長,但是我們只需要看偏移量為50的那個字節碼即可。你看到了什么,DELETE_NAME直接把e這個變量給刪了,所以我們就找不到了,因此代碼相當於下面這樣:

e = 2.718
try:
    raise Exception("我要引發異常了")
except Exception as e:
    try:
        print(e)
    finally:
        del e

因此在異常處理的時候,如果把異常賦予了一個變量,那么這個變量異常處理結束會被刪掉,因此只能在except里面使用,這就是原因。但是原因有了,可動機呢?Python這么做的動機是什么?根據官網文檔解釋:

當使用 as 將目標賦值為一個異常時,它將在 except 子句結束時被清除,這意味着異常必須賦值給一個不同的名稱(不同於外部指定的變量),才能在 except 子句之后引用它(外部指定的變量)。異常會被清除是因為在附加了回溯信息的情況下,它們會形成堆棧幀的循環引用,使得所有局部變量保持存活直到發生下一次垃圾回收。

try、except、finally的返回值問題

我們看看這三者的返回值之間的關系:

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

# 由於沒有發生異常, 所以返回了try指定的返回值
print(f1())  # 123


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

# 此時發生異常, 所以返回了except指定的返回值
print(f2())  # 456


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

# 返回的還是try指定的返回值, 因為finally中沒有指定返回值
print(f3())  # 123


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

# 一旦finally中出現了return, 那么在沒有報錯的情況下返回的都是finally指定的返回值
print(f4())  # None


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

# 我們函數一旦出現了return, 那么就表示結束函數直接返回了
# 但是return如果是在try中, 那么可以認為將返回值存起來了, 執行完finally之后再返回
# 如果finally也指定了return, 那么會返回finally指定的返回值, 否則還是返回之前的
# 總之一句話, 只要在try或者except中出現了return(前提是沒有異常、或者異常被成功捕獲)
# 那么在finally執行完畢之后, 會立即返回, 不會執行finally下面的代碼
print(f5())  # 123


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

# 沒有異常, 所以except的return沒啥卵用, 但是try和finally中也沒有return
# 所以程序會繼續往下走
print(f6())  # 789

小結

這一次我們就分析了Python的控制語句,if、for、while都比較簡單。但Python中的異常捕獲算是比較復雜的,主要是牽扯的東西比較多,有時候分析某一個地方需要跳好幾個源文件,進行查找。因此有興趣的話,可以殺進源碼中自由翱翔,但是注意Python的版本,我們說3.8版本和3.8之前的版本之間區別還是蠻大的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM