一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

一文讓你搞懂 Python 虛擬機(jī)執(zhí)行字節(jié)碼的奧秘

 古明地覺(jué)O_o 2023-03-02 發(fā)布于北京

楔子

當(dāng)解釋器啟動(dòng)后,首先會(huì)進(jìn)行運(yùn)行時(shí)環(huán)境的初始化。注意這里的運(yùn)行時(shí)環(huán)境,它和之前說(shuō)的執(zhí)行環(huán)境是不同的概念。運(yùn)行時(shí)環(huán)境是一個(gè)全局的概念,而執(zhí)行環(huán)境是一個(gè)棧幀。

關(guān)于運(yùn)行時(shí)環(huán)境的初始化是一個(gè)很復(fù)雜的過(guò)程,涉及到 Python 進(jìn)程、線程的創(chuàng)建,類型對(duì)象的完善等非常多的內(nèi)容,我們暫時(shí)先不討論。這里就假設(shè)初始化動(dòng)作已經(jīng)完成,我們已經(jīng)站在了虛擬機(jī)的門檻外面,只需要輕輕推動(dòng)第一張骨牌,整個(gè)執(zhí)行過(guò)程就像多米諾骨牌一樣,一環(huán)扣一環(huán)地展開。

在介紹字節(jié)碼的時(shí)候我們說(shuō)過(guò),解釋器可以看成是:編譯器+虛擬機(jī),編譯器負(fù)責(zé)將源代碼編譯成 PyCodeObject 對(duì)象,而虛擬機(jī)則負(fù)責(zé)執(zhí)行。整個(gè)過(guò)程如下:

所以我們的重點(diǎn)就是虛擬機(jī)是怎么執(zhí)行 PyCodeObject 對(duì)象的?整個(gè)過(guò)程是什么,掌握了這些,你對(duì)虛擬機(jī)會(huì)有一個(gè)更深的理解。


虛擬機(jī)的運(yùn)行框架

在介紹棧幀的時(shí)候我們說(shuō)過(guò),Python 是一門動(dòng)態(tài)語(yǔ)言,一個(gè)變量指向什么對(duì)象需要在運(yùn)行時(shí)才能確定,這些信息不可能靜態(tài)存儲(chǔ)在 PyCodeObject 對(duì)象中。

所以虛擬機(jī)在運(yùn)行時(shí)會(huì)基于 PyCodeObject 對(duì)象動(dòng)態(tài)創(chuàng)建出一個(gè)棧幀對(duì)象,然后在棧幀里面執(zhí)行字節(jié)碼。而創(chuàng)建棧幀,主要使用以下兩個(gè)函數(shù):

// 基于 PyCodeObject、全局名字空間、局部名字空間,創(chuàng)建棧幀
// 參數(shù)非常簡(jiǎn)單,所以它一般適用于模塊這種參數(shù)不復(fù)雜的場(chǎng)景
// 我們說(shuō)模塊也會(huì)對(duì)應(yīng)一個(gè)棧幀,并且它位于棧幀鏈的最頂層
PyObject *
PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals)
;

// 它相比 PyEval_EvalCode 多了很多的參數(shù)
// 比如里面有位置參數(shù)以及個(gè)數(shù),關(guān)鍵字參數(shù)以及個(gè)數(shù)
// 還有默認(rèn)參數(shù)以及個(gè)數(shù),閉包等等,顯然它用于函數(shù)等復(fù)雜場(chǎng)景
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)
;

但這兩個(gè)函數(shù)都屬于高層的封裝,它們最終都會(huì)調(diào)用 _PyEval_EvalCodeWithName 函數(shù)。這個(gè)函數(shù)內(nèi)部的邏輯我們就不看了,只需要知道它在執(zhí)行完畢之后棧幀就創(chuàng)建好了,而棧幀才是我們的重點(diǎn),因?yàn)榇a在執(zhí)行期間所依賴的上下文信息全部由棧幀來(lái)維護(hù)。

一旦棧幀對(duì)象初始化完畢,那么就要進(jìn)行處理了,處理的時(shí)候會(huì)調(diào)用以下兩個(gè)函數(shù)。

PyObject *
PyEval_EvalFrame(PyFrameObject *f)
;

PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
;

當(dāng)然啦,上面這兩個(gè)函數(shù)同樣屬于高層的封裝,最終會(huì)調(diào)用 _PyEval_EvalFrameDefault 函數(shù),虛擬機(jī)就是通過(guò)該函數(shù)來(lái)完成字節(jié)碼的執(zhí)行。

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag);

到目前為止總共出現(xiàn)了 6 個(gè)函數(shù),用一張圖來(lái)描述一下它們的關(guān)系:

所以 _PyEval_EvalFrameDefault 函數(shù)是虛擬機(jī)運(yùn)行的核心,并且代碼量很大。

可以看到這一個(gè)函數(shù)大概在 3100 行左右,不過(guò)也僅僅是代碼量大而已,因?yàn)樗倪壿嫼芎美斫狻?/span>

// 源代碼位于 Python/ceval.c 中
PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{  
    //......
    co = f->f_code;
    names = co->co_names;
    consts = co->co_consts;
    fastlocals = f->f_localsplus;
    freevars = f->f_localsplus + co->co_nlocals;
    next_instr = first_instr;
    if (f->f_lasti >= 0) {
        assert(f->f_lasti % sizeof(_Py_CODEUNIT) == 0);
        next_instr += f->f_lasti / sizeof(_Py_CODEUNIT) + 1;
    }
    // 棧頂指針
    stack_pointer = f->f_stacktop;
    assert(stack_pointer != NULL);
    f->f_stacktop = NULL;       
    //......
}

該函數(shù)首先會(huì)初始化一些變量,PyCodeObject 對(duì)象包含的信息不用多說(shuō),還有一個(gè)重要的動(dòng)作就是對(duì)指針 stack_pointer 進(jìn)行初始化。stack_pointer 指向運(yùn)行時(shí)棧的棧頂,關(guān)于運(yùn)行時(shí)??赡苡腥藭簳r(shí)還不理解它是做什么的,別急,馬上你就知道它的作用了。

棧幀對(duì)象有兩個(gè)重要字段是關(guān)于運(yùn)行時(shí)棧的,f_stacktop 字段指向運(yùn)行時(shí)棧的棧頂,f_valuestack 字段指向運(yùn)行時(shí)棧的棧底。

所以對(duì) stack_pointer 初始化的時(shí)候,將它的值初始化為 f->f_stacktop,讓它指向運(yùn)行時(shí)棧的棧頂。但操作運(yùn)行時(shí)棧是通過(guò) stack_pointer 操作的,隨著元素的添加和刪除,棧頂位置會(huì)變,所以后續(xù)它反過(guò)來(lái)還要再賦值給 f_stacktop。

然后棧幀中的 f_code 就是 PyCodeObject 對(duì)象,該對(duì)象的 co_code 字段則保存著字節(jié)碼指令序列。而虛擬機(jī)執(zhí)行字節(jié)碼就是從頭到尾遍歷整個(gè) co_code,對(duì)指令逐條執(zhí)行的過(guò)程。

估計(jì)有人對(duì)棧幀和 PyCodeObject 對(duì)象的底層結(jié)構(gòu)已經(jīng)記不太清了,這里為了方便后續(xù)內(nèi)容的理解,我們將它們的結(jié)構(gòu)再展示一下。

所以字節(jié)碼也叫作指令序列,它就是一個(gè)普普通通的 bytes 對(duì)象,對(duì)于 C 而言則是一個(gè)字符數(shù)組,一條指令就是一個(gè)字符、或者說(shuō)一個(gè)整數(shù)。而在遍歷的時(shí)候會(huì)使用以下兩個(gè)變量:

  • first_instr:永遠(yuǎn)指向字節(jié)碼指令序列的第一條字節(jié)碼指令;

  • next_instr:永遠(yuǎn)指向下一條待執(zhí)行的字節(jié)碼指令;

當(dāng)然別忘記棧幀的 f_lasti 成員,它記錄了上一條已經(jīng)執(zhí)行過(guò)的字節(jié)碼指令的偏移量。

多說(shuō)一句,生成器之所以能夠從中斷的位置恢復(fù)執(zhí)行,就是因?yàn)?f_lasti 記錄了上一條執(zhí)行的字節(jié)碼指令的位置。

那么這個(gè)動(dòng)作是如何一步步完成的呢?其實(shí)就是一個(gè) for 循環(huán)加上一個(gè)巨大的 switch case 結(jié)構(gòu)。

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{   
    //......   
    co = f->f_code;
    names = co->co_names;
    consts = co->co_consts;
    fastlocals = f->f_localsplus;
    freevars = f->f_localsplus + co->co_nlocals;
    //......
  
    // 死循環(huán)
    for (;;) {
        if (_Py_atomic_load_relaxed(eval_breaker)) {
        // 不斷地讀取 co->co_code 中下一條待執(zhí)行的字節(jié)碼指令
            opcode = _Py_OPCODE(*next_instr);
        // opcode 就是字節(jié)碼指令序列中的每一條指令
        // 指令有哪些都定義在 Include/opcode.h 中
            if (opcode == SETUP_FINALLY ||
                opcode == SETUP_WITH ||
                opcode == BEFORE_ASYNC_WITH ||
                opcode == YIELD_FROM) {
                goto fast_next_opcode; 
            }

        fast_next_opcode:
            // ......
            // 判斷該指令屬于什么操作,然后執(zhí)行相應(yīng)的邏輯
            switch (opcode) {
                // 加載一個(gè)局部變量
                case TARGET(LOAD_FAST):
                    // ......
                    break
;
                // 加載一個(gè)常量
                case TARGET(LOAD_CONST):
                    // ......
                    break
;
                // ......
        }
    }
}

在這個(gè)執(zhí)行架構(gòu)中,對(duì)字節(jié)碼的遍歷是通過(guò)宏來(lái)實(shí)現(xiàn)的:

#define INSTR_OFFSET()  \
    (sizeof(_Py_CODEUNIT) * (int)(next_instr - first_instr))


#define NEXTOPARG()  do { \
        _Py_CODEUNIT word = *next_instr; \
        opcode = _Py_OPCODE(word); \
        oparg = _Py_OPARG(word); \
        next_instr++; \
    } while (0)

首先每條字節(jié)碼指令都會(huì)帶有一個(gè)參數(shù),co_code 中索引為 0 2 4 6 8... 的整數(shù)便是指令,索引為 1 3 5 7 9... 的整數(shù)便是參數(shù)。所以 co_code 里面并不全是字節(jié)碼指令,每條指令后面都還跟著一個(gè)參數(shù)。因此 next_instr 每次向后移動(dòng)兩個(gè)字節(jié),便可跳到下一條指令。

next_instr 和 first_instr 都是  _Py_CODEUNIT *  類型的變量,這個(gè) _Py_CODEUNIT 是一個(gè) uint16_t。所以只要執(zhí)行 next_instr++,便可向后移動(dòng)兩字節(jié),跳到下一條指令。

我們?cè)倏匆幌律厦娴暮辏琁NSTR_OFFSET 計(jì)算的顯然就是下一條待執(zhí)行的指令和第一條指令之間的偏移量;然后是 NEXTOPARG,里面的變量 word 就是待執(zhí)行的指令。

當(dāng)然,由于 word 占兩字節(jié),所以也包括了參數(shù)。其中 word 的前 8 位是指令 opcode,后 8 位是參數(shù) oparg。然后在解析出來(lái)指令以及參數(shù)之后,再執(zhí)行 next_instr++,跳到下一條指令。

而接下來(lái)就要執(zhí)行上面剛解析出來(lái)的字節(jié)碼指令了,會(huì)利用 switch 語(yǔ)句對(duì)指令進(jìn)行判斷,根據(jù)判斷的結(jié)果選擇不同的 case 分支

每一個(gè) case 分支,對(duì)應(yīng)一個(gè)字節(jié)碼指令的實(shí)現(xiàn),不同的指令執(zhí)行不同的 case 分支。所以這個(gè) switch case 語(yǔ)句非常的長(zhǎng),函數(shù)總共 3000 多行,這個(gè) switch 就占了2400行。因?yàn)橹噶罘浅6?,比如:LOAD_CONST, LOAD_NAME, YIELD_FROM等等,而每一個(gè)指令都要對(duì)應(yīng)一個(gè) case 分支。

然后當(dāng)匹配到的 case 分支執(zhí)行完畢時(shí),說(shuō)明當(dāng)前的這一條字節(jié)碼指令就執(zhí)行完畢了,那么虛擬機(jī)的執(zhí)行流程會(huì)跳轉(zhuǎn)到標(biāo)簽 fast_next_opcode 所在位置,或者 for 循環(huán)所在位置。但不管如何,虛擬機(jī)接下來(lái)的動(dòng)作就是獲取下一條字節(jié)碼指令和指令參數(shù),完成對(duì)下一條指令的執(zhí)行。

所以通過(guò) for 循環(huán)一條一條遍歷 co_code 中包含的所有字節(jié)碼指令,然后交給內(nèi)部的 switch 語(yǔ)句、選擇不同的 case 分支進(jìn)行執(zhí)行,如此周而復(fù)始,最終完成了對(duì)整個(gè) Python 程序的執(zhí)行。

盡管目前只是簡(jiǎn)單的分析,但相信你也能大體地了解 Python 執(zhí)行引擎的整體結(jié)構(gòu)。說(shuō)白了 Python 虛擬機(jī)就是將自己當(dāng)成一個(gè) CPU,在棧幀中一條條的執(zhí)行指令,而執(zhí)行過(guò)程中所依賴的常量、變量等,則由棧幀的其它成員來(lái)維護(hù)。

因此在虛擬機(jī)的執(zhí)行流程進(jìn)入了那個(gè)巨大的 for 循環(huán),并取出第一條字節(jié)碼指令交給里面的 switch 語(yǔ)句之后,第一張多米諾骨牌就已經(jīng)被推倒,命運(yùn)不可阻擋的降臨了。一條接一條的指令如同潮水般涌來(lái),浩浩蕩蕩,橫無(wú)際涯。


運(yùn)行時(shí)棧的一些 API

這里先來(lái)簡(jiǎn)單介紹一下運(yùn)行時(shí)棧,它是參數(shù)的容身之所,比如虛擬機(jī)在執(zhí)行 a + b 的時(shí)候,知道這是一個(gè)加法操作。但在執(zhí)行加法的時(shí)候,加號(hào)兩邊的值是多少,它要怎么獲取呢?這時(shí)候就需要一個(gè)棧來(lái)專門保存相應(yīng)的參數(shù)。

在執(zhí)行加法之前,先將 a 和 b 壓入棧中,然后執(zhí)行加法的時(shí)候,再將 a 和 b 從棧里面彈出來(lái)即可。現(xiàn)在有一個(gè)印象,一會(huì)兒我們通過(guò)反編譯查看字節(jié)碼指令的時(shí)候,就一切都清晰了。

然后再來(lái)看看運(yùn)行時(shí)棧相關(guān)的一些 API。

API 非常多,但操作運(yùn)行時(shí)棧都是通過(guò)操作 stack_pointer 實(shí)現(xiàn)的。假設(shè)運(yùn)行時(shí)棧內(nèi)部有三個(gè)元素,從棧底到棧頂分別是整數(shù) 1、2、3,那么運(yùn)行時(shí)棧的結(jié)構(gòu)就是下面這樣。

然后看一下這些和運(yùn)行時(shí)棧相關(guān)的 API 都是干嘛的。

STACK_LEVEL():

#define STACK_LEVEL() \
     ((int)(stack_pointer - f->f_valuestack))

返回運(yùn)行時(shí)棧的元素?cái)?shù)量。

EMPTY():

#define EMPTY()      (STACK_LEVEL() == 0)

判斷運(yùn)行時(shí)棧是否為空。

TOP():

#define TOP()          (stack_pointer[-1])

查看當(dāng)前運(yùn)行時(shí)棧的棧頂元素。

SECOND():

#define SECOND()       (stack_pointer[-2])

查看從棧頂元素開始的第二個(gè)元素,所以隨著元素不斷添加,棧頂元素也在不斷發(fā)生變化,而 stack_pointer 也在不斷變化。

THIRD():

#define THIRD()        (stack_pointer[-3])

查看從棧頂元素開始的第三個(gè)元素。

FOURTH():

#define FOURTH()      (stack_pointer[-4])

查看從棧頂元素開始的第四個(gè)元素。

PEEK(n):

#define PEEK(n)       (stack_pointer[-(n)])

查看從棧頂元素開始的第 n 個(gè)元素。

SET_TOP(v):

#define SET_TOP(v)    (stack_pointer[-1] = (v))

將當(dāng)前運(yùn)行時(shí)棧的棧頂元素設(shè)置成 v,同理還有 SET_SECOND,SET_THIRD,SET_FOURTH,SET_VALUE。

PUSH(v):

往運(yùn)行時(shí)棧中壓入一個(gè)元素。

// 將 stack_pointer 設(shè)置成 v,然后再執(zhí)行自增操作
#define BASIC_PUSH(v)   (*stack_pointer++ = (v))
// 也是調(diào)用了 BASIC_PUSH,但做了一層檢測(cè)
// co_stacksize 表示運(yùn)行時(shí)棧的大小,STACK_LEVEL() 不能超過(guò)它
#define PUSH(v)         { (void)(BASIC_PUSH(v), \
                          lltrace && prtrace(tstate, TOP(), "push")); \
                          assert(STACK_LEVEL() <= co->co_stacksize); }

假設(shè)當(dāng)前運(yùn)行時(shí)棧有 1、2、3 總共三個(gè)元素,我們往棧里面壓入一個(gè)元素 4,那么運(yùn)行時(shí)棧就會(huì)變成下面這個(gè)樣子。

Python 的變量都是一個(gè)指針,所以 stack_pointer 是一個(gè)二級(jí)指針,它永遠(yuǎn)指向棧頂位置,只不過(guò)棧頂位置會(huì)變。

POP(v):

從運(yùn)行時(shí)棧彈出一個(gè)元素,注意它和 TOP 的區(qū)別,TOP 是返回棧頂元素,但不彈出。

// 將 stack_pointer 先執(zhí)行自減操作,然后解引用
#define BASIC_POP()    (*--stack_pointer)
#define POP()       ((void)(lltrace && prtrace(tstate, TOP(), "pop")), \
                     BASIC_POP())

假設(shè)當(dāng)前運(yùn)行時(shí)棧有 1、2、3 總共三個(gè)元素,我們彈出一個(gè)元素,那么運(yùn)行時(shí)棧就會(huì)變成下面這個(gè)樣子。

stack_pointer 指向棧頂位置,所以它向棧底移動(dòng)一個(gè)位置,就相當(dāng)于元素被彈出了。


通過(guò)反編譯查看字節(jié)碼

我們寫一段簡(jiǎn)單的代碼,然后反編譯,看看虛擬機(jī)是如何執(zhí)行字節(jié)碼的。

code = """\
chinese = 89
math = 99
english = 91
avg = (chinese + math + english) / 3
"""


# 將上面的代碼以模塊的方式進(jìn)行編譯
co = compile(code, "...""exec")
# 查看常量池
print(co.co_consts)  # (89, 99, 91, 3, None)
# 查看符號(hào)表
print(
    co.co_names
)  # ('chinese', 'math', 'english', 'avg')

在編譯的時(shí)候,常量和符號(hào)(變量)都會(huì)被靜態(tài)收集起來(lái),然后我們反編譯一下看看字節(jié)碼,直接通過(guò) dis.dis(co) 即可。結(jié)果如下:

  1      0 LOAD_CONST               0 (89)
         2 STORE_NAME               0 (chinese)

  2      4 LOAD_CONST               1 (99)
         6 STORE_NAME               1 (math)

  3      8 LOAD_CONST               2 (91)
        10 STORE_NAME               2 (english)

  4     12 LOAD_NAME                0 (chinese)
        14 LOAD_NAME                1 (math)
        16 BINARY_ADD
        18 LOAD_NAME                2 (english)
        20 BINARY_ADD
        22 LOAD_CONST               3 (3)
        24 BINARY_TRUE_DIVIDE
        26 STORE_NAME               3 (avg)
        28 LOAD_CONST               4 (None)
        30 RETURN_VALUE

解釋一下每一列的含義:

  • 第一列是源代碼的行號(hào);

  • 第二列是指令的偏移量,或者說(shuō)該指令在整個(gè)字節(jié)碼指令序列中的索引。因?yàn)槊織l指令后面都跟著一個(gè)參數(shù),所以偏移量是 0 2 4 6 8 ...;

  • 第三列是字節(jié)碼指令,簡(jiǎn)稱指令,它們?cè)诤甓x中代表整數(shù);

  • 第四列是字節(jié)碼指令參數(shù),簡(jiǎn)稱指令參數(shù)、或者參數(shù),不同的指令參數(shù)的含義不同;

  • 第五列是 dis 模塊給我們額外提供的信息,一會(huì)說(shuō);

我們從上到下依次解釋每條指令都干了什么?

0 LOAD_CONST:表示加載一個(gè)常量(指針),并壓入運(yùn)行時(shí)棧。后面的指令參數(shù) 0 表示從常量池中加載索引為 0 的常量,至于 89 則表示加載的常量是 89。所以最后面的括號(hào)里面的內(nèi)容實(shí)際上起到的是一個(gè)提示作用,告訴你加載的對(duì)象是什么。

2 STORE_NAME:表示將 LOAD_CONST 加載的常量用一個(gè)名字綁定起來(lái),放在所在的名字空間中。后面的 0 (chinese) 則表示使用符號(hào)表中索引為 0 的名字(符號(hào)),且名字為 "chinese"。

所以像 chinese = 89 這種簡(jiǎn)單的賦值語(yǔ)句,會(huì)對(duì)應(yīng)兩條字節(jié)碼指令。

然后 4 LOAD_CONST、6 STORE_NAME 和 8 LOAD_CONST、10 STORE_NAME 的作用顯然和上面是一樣的,都是加載一個(gè)常量,然后將某個(gè)符號(hào)和常量綁定起來(lái),并放在名字空間中。

12 LOAD_NAME:加載一個(gè)變量,并壓入運(yùn)行時(shí)棧。而后面的 0 (chinese) 表示加載符號(hào)表中索引為 0 的變量的值,然后這個(gè)變量叫 chinese。14 LOAD_NAME 也是同理,將符號(hào)表中索引為 1 的變量的值壓入運(yùn)行時(shí)棧,并且變量叫 math。此時(shí)棧里面有兩個(gè)元素,從棧底到棧頂分別是 chinese 和 math。

16 BINARY_ADD:將上面兩個(gè)變量從運(yùn)行時(shí)棧彈出,然后執(zhí)行加法操作,并將結(jié)果壓入運(yùn)行時(shí)棧。

18 LOAD_NAME:將符號(hào)表中索引為 2 的變量 english 的值壓入運(yùn)行時(shí)棧,此時(shí)棧里面有兩個(gè)元素,從棧底到棧頂分別是 chinese + math 的返回結(jié)果english

20 BINARY_ADD:將運(yùn)行時(shí)棧里的兩個(gè)元素彈出,然后執(zhí)行加法操作,并將結(jié)果壓入運(yùn)行時(shí)棧。此時(shí)棧里面有一個(gè)元素,就是 chinese + math + english 的返回結(jié)果。

22 LOAD_CONST:將常量 3 壓入運(yùn)行時(shí)棧,此時(shí)棧里面有兩個(gè)元素;

22 BINARY_TRUE_DIVIDE:將運(yùn)行時(shí)棧里的兩個(gè)元素彈出,然后執(zhí)行除法操作,并將結(jié)果壓入運(yùn)行時(shí)棧,此時(shí)棧里面有一個(gè)元素;

24 STORE_NAME:將元素從運(yùn)行時(shí)棧里面彈出,并用符號(hào)表中索引為 3 的變量 avg 和它綁定起來(lái),然后放在名字空間中。

28 LOAD_CONST:將常量 None 壓入運(yùn)行時(shí)棧,然后通過(guò) 30 RETURN_VALUE 將其從棧中彈出,然后返回。

所以 Python 虛擬機(jī)就是把自己想象成一顆 CPU,在棧幀中一條條執(zhí)行字節(jié)碼指令,當(dāng)指令執(zhí)行完畢或執(zhí)行出錯(cuò)時(shí),停止執(zhí)行。

我們通過(guò)幾張圖展示一下上面的過(guò)程,為了閱讀方便,這里將相應(yīng)的源代碼再貼一份:

chinese = 89
math = 99
english = 91
avg = (chinese + math + english) / 3

我們說(shuō)模塊也有自己的作用域,并且是全局作用域,所以虛擬機(jī)也會(huì)為它創(chuàng)建棧幀。而在代碼還沒(méi)有執(zhí)行的時(shí)候,棧幀就已經(jīng)創(chuàng)建好了,整個(gè)布局如下。

這里補(bǔ)充一個(gè)知識(shí)點(diǎn),非常重要,首先我們看到棧幀里面有一個(gè) f_localsplus 屬性,它是一個(gè)數(shù)組。雖然聲明的時(shí)候?qū)懼L(zhǎng)度為 1,但實(shí)際使用時(shí),長(zhǎng)度不受限制,和 Go 語(yǔ)言不同,C 數(shù)組的長(zhǎng)度不屬于類型的一部分。

所以 f_localsplus 是一個(gè)動(dòng)態(tài)內(nèi)存,運(yùn)行時(shí)棧所需要的空間就存儲(chǔ)在里面。但這塊內(nèi)存并不光給運(yùn)行時(shí)棧使用,它被分成了四塊。

函數(shù)的局部變量是靜態(tài)存儲(chǔ)的,那么都存在哪呢?答案是在 f_localsplus 里面,而且是開頭的位置。在獲取的時(shí)候直接基于索引操作即可,因此速度會(huì)更快。所以源碼內(nèi)部還有兩個(gè)宏:

fastlocals 就是棧幀的 f_localsplus,而函數(shù)在編譯的時(shí)候就知道某個(gè)局部變量在 f_localsplus 中的索引,所以通過(guò) GETLOCAL 獲取即可。同理 SETLOCAL 則是創(chuàng)建一個(gè)局部變量。

至于 cell 對(duì)象和 free 對(duì)象則是用來(lái)處理閉包的,而 f_localsplus 的最后一塊內(nèi)存則用于運(yùn)行時(shí)棧。

所以 f_localsplus 是一個(gè)數(shù)組,它是一段連續(xù)內(nèi)存,只不過(guò)從邏輯上講,它被分成了四份,每一份用在不同的地方。但它們整體是連續(xù)的,都是數(shù)組的一部分。按照新一團(tuán)團(tuán)長(zhǎng)丁偉的說(shuō)法:彼此是雞犬相聞,但又老死不相往來(lái)。

但我們當(dāng)前是以模塊的方式編譯的,里面所有的變量都是全局變量,而且也不涉及閉包啥的,所以這里就把 f_localsplus 理解為運(yùn)行時(shí)棧即可。

接下來(lái)就開始執(zhí)行字節(jié)碼了,next_instr 指向下一條待執(zhí)行的字節(jié)碼指令,顯然初始狀態(tài)下,下一條待執(zhí)行的指令就是第一條指令。

于是虛擬機(jī)開始加載:0 LOAD_CONST,該指令表示將常量加載進(jìn)運(yùn)行時(shí)棧,而要加載的常量在常量池中的索引,由指令參數(shù)表示。

在源碼中,指令對(duì)應(yīng)的變量是 opcode,指令參數(shù)對(duì)應(yīng)的變量是 oparg

// 代碼位于 Python/ceval.c 中
case TARGET(LOAD_CONST){
    // 調(diào)用元組的 GETITEM 方法
    // 從常量池中加載索引為 oparg 的對(duì)象(常量)
    // 當(dāng)然啦,這里為了方便稱其為對(duì)象,但其實(shí)是指向?qū)ο蟮闹羔?/span>
    PREDICTED(LOAD_CONST);
    PyObject *value = GETITEM(consts, oparg);
    // 增加引用計(jì)數(shù)
    Py_INCREF(value);
    // 壓入運(yùn)行時(shí)棧
    PUSH(value);
    FAST_DISPATCH();
}

該指令的參數(shù)為 0,所以會(huì)將常量池中索引為 0 的元素 89 壓入運(yùn)行時(shí)棧,執(zhí)行完之后,棧幀的布局就變成了下面這樣:

f_localsplus 下面的箭頭方向,代表運(yùn)行時(shí)棧從棧底到棧頂?shù)姆较颉?/span>

接著虛擬機(jī)執(zhí)行 2 STORE_NAME 指令,從符號(hào)表中獲取索引為 0 的符號(hào)、即 chinese。然后將棧頂元素 89 彈出,再將符號(hào) chinese 整數(shù)對(duì)象 89 綁定起來(lái)保存到 local 名字空間中。

case TARGET(STORE_NAME){
    // 從符號(hào)表中加載索引為 oparg 的符號(hào)  
    // 符號(hào)本質(zhì)上就是一個(gè) PyUnicodeObject 對(duì)象
    // 這里就是字符串 "chinese"
    PyObject *name = GETITEM(names, oparg);
    // 從運(yùn)行時(shí)棧的棧頂彈出元素
    // 顯然是上一步壓入的 89
    PyObject *v = POP();
    // 獲取名字空間 namespace
    PyObject *ns = f->f_locals;
    int err;
    if (ns == NULL) {
     // 如果沒(méi)有名字空間則報(bào)錯(cuò),設(shè)置異常
     // 這個(gè) tstate 是和線程密切相關(guān)的
        _PyErr_Format(tstate, PyExc_SystemError,
                      "no locals found when storing %R", name);
        Py_DECREF(v);
        goto error;
    }
    // 將符號(hào)和對(duì)象綁定起來(lái)放在 ns 中
    // 名字空間是一個(gè)字典,PyDict_CheckExact 則檢測(cè) ns 是否為字典
    if (PyDict_CheckExact(ns))
        // PyDict_CheckExact(ns) 類似于 type(ns) is dict
        // 除此之外,還有 PyDict_Check(ns)
        // 它類似于 isinstance(ns, dict),檢測(cè)標(biāo)準(zhǔn)相對(duì)要寬松一些
        // 然后將鍵值對(duì) "chinese": 89 設(shè)置到字典中
        err = PyDict_SetItem(ns, name, v);
    else
        // 走到這里說(shuō)明 type(ns) 不是 dict,那么它應(yīng)該繼承 dict
        // 設(shè)置元素
        err = PyObject_SetItem(ns, name, v);
        
    // 對(duì)象的引用計(jì)數(shù)減 1,因?yàn)閺倪\(yùn)行時(shí)棧中彈出了
    Py_DECREF(v);
    // 如果 err != 0,證明設(shè)置元素出錯(cuò)了,跳轉(zhuǎn)至 error 標(biāo)簽
    if (err != 0)
        goto error;
    DISPATCH();
}

執(zhí)行完之后,棧幀的布局就變成了下面這樣:

此時(shí)運(yùn)行時(shí)棧為空,local 名字空間多了個(gè)鍵值對(duì)。

同理剩余的兩個(gè)賦值語(yǔ)句也是類似的,只不過(guò)指令參數(shù)不同,比如 6 STORE_NAME 加載的是符號(hào)表中索引為 1 的符號(hào),8 STORE_NAME 加載的是符號(hào)表中索引為 2 的符號(hào),分別是 math 和 english。

然后 12 LOAD_NAME14 LOAD_NAME 負(fù)責(zé)將符號(hào)表中索引為 0 和 1 的變量的值壓入運(yùn)行時(shí)棧:

case TARGET(LOAD_NAME){
    // 從符號(hào)表 co_names 中加載索引為 oparg 的變量
    // 但是注意:全局變量是通過(guò)字典存儲(chǔ)的
    // 所以這里的 name 只是一個(gè)字符串罷了,比如 "chinese"
    // 然后還要再根據(jù)這個(gè)字符串從字典里面查找對(duì)應(yīng)的 value
    PyObject *name = GETITEM(names, oparg);
    // 對(duì)于模塊來(lái)說(shuō),f->f_locals 和 f->f_globals 指向同一個(gè)字典
    PyObject *locals = f->f_locals;
    PyObject *v;
    // ....
    if (PyDict_CheckExact(locals)) {
        // 根據(jù) name 獲取 value
        // 所以 print(chinese) 本質(zhì)上就是下面這樣
        // print(locals["chinese"])
        v = PyDict_GetItemWithError(locals, name);
        if (v != NULL) {
            Py_INCREF(v);
        }
        else if (_PyErr_Occurred(tstate)) {
            goto error;
        }
    }
    else {
        // ...
    }
    // ...
    // 將符號(hào)表中索引為 oparg 的變量的值
    // 壓入運(yùn)行時(shí)棧
    PUSH(v);
    DISPATCH();
}

上面兩條指令執(zhí)行完之后,棧幀的布局就變成了下面這樣:

接下來(lái)執(zhí)行 16 BINARY_ADD,它會(huì)將棧里的兩個(gè)元素彈出,然后執(zhí)行加法操作,最后再將結(jié)果入棧。

當(dāng)然上面這種說(shuō)法是為了方便理解,其實(shí)虛擬機(jī)真正執(zhí)行的時(shí)候,只會(huì)彈出一個(gè)元素,而另一個(gè)元素只是使用 TOP() 進(jìn)行查看,但不彈出。將結(jié)果計(jì)算完畢之后,再將棧頂元素替換掉。 

所以本質(zhì)上,和彈出兩個(gè)元素、再將計(jì)算結(jié)果入棧是一樣的。

case TARGET(BINARY_ADD){
    // 從棧頂彈出元素,這里是 99(變量 math),
    PyObject *right = POP();
    // math 彈出之后,chinese 就成為了新的棧頂元素
    // 這里的 TOP() 則是獲取棧頂元素 89(變量 chinese)
    PyObject *left = TOP();
    // 用于保存兩者的和
    PyObject *sum;
    
    // 如果是字符串,執(zhí)行專門的函數(shù)
    if (PyUnicode_CheckExact(left) &&
             PyUnicode_CheckExact(right)) {
        sum = unicode_concatenate(tstate, left, right, f, next_instr);
    }
    // 否則通過(guò)泛型 API PyNumber_Add 進(jìn)行計(jì)算
    else {
        sum = PyNumber_Add(left, right);
        Py_DECREF(left);
    }
    // 從棧里面彈出,所以減少引用計(jì)數(shù)
    Py_DECREF(right);
    // 將棧頂元素替換成 sum
    SET_TOP(sum);
    if (sum == NULL)
        goto error;
    DISPATCH();
}

BINARY_ADD 指令執(zhí)行完之后,棧幀的布局就變成了下面這樣:

然后 18 LOAD_NAME 負(fù)責(zé)將符號(hào)表中索引為 2 的變量 english 的值壓入運(yùn)行時(shí)棧;而指令 20 BINARY_ADD 則是繼續(xù)執(zhí)行加法操作,并將結(jié)果設(shè)置在棧頂;然后 22 LOAD_CONST 將常量 3 再壓入運(yùn)行時(shí)棧。

這三條指令執(zhí)行之后,運(yùn)行時(shí)棧變化如下:

接著是 24 BINARY_TRUE_DIVIDE,它的邏輯和 BINARY_ADD 類似,只不過(guò)一個(gè)執(zhí)行除法,一個(gè)執(zhí)行加法。

case TARGET(BINARY_TRUE_DIVIDE){
    // 從棧頂彈出元素,顯然是 3
    PyObject *divisor = POP();
    // 查看棧頂元素,此時(shí)棧頂元素變成了 279
    PyObject *dividend = TOP();
    // 調(diào)用 PyNumber_TrueDivide,執(zhí)行 279 / 3
    PyObject *quotient = PyNumber_TrueDivide(dividend, divisor);
    // 從棧里面彈出,減少引用計(jì)數(shù)
    Py_DECREF(dividend);
    Py_DECREF(divisor);
    // 將棧頂元素替換為 279 / 3 的計(jì)算結(jié)果
    SET_TOP(quotient);
    if (quotient == NULL)
        goto error;
    DISPATCH();
}

24 BINARY_TRUE_DIVIDE 執(zhí)行完之后,運(yùn)行時(shí)棧如下:

然后 26 STORE_NAME 將棧頂元素 93.0 彈出,并將符號(hào)表中索引為 3 的變量 avg 和它綁定起來(lái),放到名字空間中。因此最終棧幀關(guān)系圖如下:

以上就是虛擬機(jī)對(duì)這幾行代碼的執(zhí)行流程,整個(gè)過(guò)程就像 CPU 執(zhí)行指令一樣。

我們?cè)儆?Python 代碼描述一遍上面的邏輯:

# LOAD_CONST 將 89 壓入棧中
# STORE_NAME 將 89 從棧中彈出
# 并將符號(hào) "chinese" 和 89 綁定起來(lái),放在名字空間中
chinese = 89
print(
    {k: v for k, v in locals().items() if not k.startswith("__")}
)  # {'chinese': 89}

math = 99
print(
    {k: v for k, v in locals().items() if not k.startswith("__")}
)  # {'chinese': 89, 'math': 99}

english = 91
print(
    {k: v for k, v in locals().items() if not k.startswith("__")}
)  # {'chinese': 89, 'math': 99, 'english': 91}

avg = (chinese + math + english) / 3
print(
    {k: v for k, v in locals().items() if not k.startswith("__")}
)  # {'chinese': 89, 'math': 99, 'english': 91, 'avg': 93.0}

現(xiàn)在你是不是對(duì)虛擬機(jī)執(zhí)行字節(jié)碼有一個(gè)更深的了解了呢?當(dāng)然字節(jié)碼指令有很多,不止我們上面看到的那幾個(gè)。你可以隨便寫一些代碼,然后分析一下它的字節(jié)碼指令是什么樣的。

指令都定義在 Include/opcode.h 中


變量賦值時(shí)用到的指令

這里我們來(lái)介紹幾個(gè)在變量賦值的時(shí)候,所用到的指令。因?yàn)槌霈F(xiàn)頻率極高,所以有必要單獨(dú)說(shuō)一下。

下面來(lái)實(shí)際操作一波,看看這些指令:

0 LOAD_CONST:加載字符串常量 "female";

2 STORE_FAST:在局部作用域中定義一個(gè)局部變量 gender,和字符串對(duì)象 "female" 建立映射關(guān)系,本質(zhì)上就是讓變量 gender 保存這個(gè)字符串對(duì)象的地址;

4 LOAD_GLOBAL:在局部作用域中加載一個(gè)內(nèi)置變量 print;

6 LOAD_FAST:在局部作用域中加載一個(gè)局部變量 gender;

14 LOAD_GLOBAL:在局部作用域中加載一個(gè)全局變量 name;

0 LOAD_CONST:加載字符串常量 "古明地戀";

2 STORE_GLOBAL:在局部作用域中定義一個(gè)被 global 關(guān)鍵字聲明的全局變量;

0 LOAD_CONST:加載字符串常量 "古明地覺(jué)";

2 STORE_NAME:在全局作用域中定義一個(gè)全局變量 name,并和上面的字符串對(duì)象進(jìn)行綁定;

4 LOAD_NAME:在全局作用域中加載一個(gè)內(nèi)置變量 print;

6 LOAD_NAME:在全局作用域中加載一個(gè)全局變量 name;

以上我們就通過(guò)代碼實(shí)際演示了這些指令的作用,它們和常量、變量的加載,以及變量的定義密切相關(guān),可以說(shuō)常見(jiàn)的不能再常見(jiàn)了。你寫的任何代碼在反編譯之后都少不了它們的身影,因此有必要提前解釋一下。

不管加載的是常量、還是變量,得到的永遠(yuǎn)是指向?qū)ο蟮闹羔槨?/span>


變量賦值的具體細(xì)節(jié)

這里再通過(guò)變量賦值感受一下字節(jié)碼的執(zhí)行過(guò)程,首先關(guān)于變量賦值,你平時(shí)是怎么做的呢?

這些賦值語(yǔ)句背后的原理是什么呢?我們通過(guò)字節(jié)碼來(lái)逐一回答。

1)a, b = b, a 的背后原理是什么?

想要知道背后的原理,查看它的字節(jié)碼是我們最好的選擇。

  0 LOAD_NAME                0 (b)
  2 LOAD_NAME                1 (a)
  4 ROT_TWO
  6 STORE_NAME               1 (a)
  8 STORE_NAME               0 (b)

里面關(guān)鍵的就是 ROT_TWO 指令,雖然我們還沒(méi)看這個(gè)指令,但也能猜出來(lái)它負(fù)責(zé)交換棧里面的兩個(gè)元素。假設(shè) a 和 b 的值分別為 22、33,整個(gè)過(guò)程如下:

來(lái)看一下 ROT_TWO 指令。

case TARGET(ROT_TWO){
    // 獲取棧頂元素,由于 b 先入棧、a 后入棧
    // 再加上棧是先入后出,所以這里獲取的棧頂元素就是 a
    PyObject *top = TOP();
    // 運(yùn)行時(shí)棧的第二個(gè)元素就是 b
    // TOP 是查看棧頂元素、SECOND 是查看棧的第二個(gè)元素
    // 并且這兩個(gè)宏只是獲取,不會(huì)將元素從棧中彈出
    PyObject *second = SECOND();
    // 將棧頂元素設(shè)置為 second,這里顯然就是變量 b
    // 將棧的第二個(gè)元素設(shè)置為 top,這里顯然就是變量 a
    SET_TOP(second);
    SET_SECOND(top);
    FAST_DISPATCH();
}

因此執(zhí)行完 ROT_TWO 指令之后,棧頂元素就是 b,棧的第二個(gè)元素就是 a。然后后面的兩個(gè) STORE_NAME 會(huì)將棧里面的元素 b、a 依次彈出,賦值給 a、b,從而完成變量交換。

2)a, b, c = c, b, a 的背后原理是什么?

老規(guī)矩,還是查看字節(jié)碼,因?yàn)橐磺姓嫦喽茧[藏在字節(jié)碼當(dāng)中。

  0 LOAD_NAME                0 (c)
  2 LOAD_NAME                1 (b)
  4 LOAD_NAME                0 (a)
  6 ROT_THREE
  8 ROT_TWO
 10 STORE_NAME               2 (a)
 12 STORE_NAME               1 (b)
 14 STORE_NAME               0 (c)

整個(gè)過(guò)程和 a, b = b, a 是相似的,首先 LOAD_NAME 將變量 c、b、a 依次壓入棧中。由于棧先入后出的特性,此時(shí)棧的三個(gè)元素按照順序(從棧頂?shù)綏5祝┓謩e是 a、b、c。

然后是 ROT_THREE 和 ROT_TWO,毫無(wú)疑問(wèn),這兩個(gè)指令執(zhí)行完之后,會(huì)將棧的三個(gè)元素調(diào)換順序,也就是將 a、b、c 變成 c、b、a。

最后 STORE_NAME 將棧的三個(gè)元素 c、b、a 依次彈出,分別賦值給 a、b、c,從而完成變量的交換。

因此核心就在 ROT_THREE 和 ROT_TWO 上面,由于后者上面已經(jīng)說(shuō)過(guò)了,所以我們看一下 ROT_THREE。

case TARGET(ROT_THREE){
    PyObject *top = TOP();
    PyObject *second = SECOND();
    PyObject *third = THIRD();
    SET_TOP(second);
    SET_SECOND(third);
    SET_THIRD(top);
    FAST_DISPATCH();
}

棧頂元素是 top、棧的第二個(gè)元素是 second、棧的第三個(gè)元素是 third,然后將棧頂元素設(shè)置為 second、棧的第二個(gè)元素設(shè)置為 third、棧的第三個(gè)元素設(shè)置為 top。

所以棧里面的 a、b、c 在經(jīng)過(guò) ROT_THREE 之后就變成了 b、c、a,顯然這還不是正確的結(jié)果。于是繼續(xù)執(zhí)行 ROT_TWO,將棧的前兩個(gè)元素進(jìn)行交換,執(zhí)行完之后就變成了 c、b、a。

假設(shè) a、b、c 的值分別為 "a"、"b"、"c",整個(gè)過(guò)程如下:

3)a, b, c, d = d, c, b, a 的背后原理是什么?它和上面提到的 1)和 2)有什么區(qū)別呢?

我們還是看一下字節(jié)碼。

  0 LOAD_NAME                0 (d)
  2 LOAD_NAME                1 (c)
  4 LOAD_NAME                2 (b)
  6 LOAD_NAME                3 (a)
  8 BUILD_TUPLE              4
 10 UNPACK_SEQUENCE          4
 12 STORE_NAME               3 (a)
 14 STORE_NAME               2 (b)
 16 STORE_NAME               1 (c)
 18 STORE_NAME               0 (d)

依舊是將等號(hào)右邊的變量,按照從左往右的順序,依次壓入棧中,但此時(shí)沒(méi)有直接將棧里面的元素做交換,而是構(gòu)建一個(gè)元組。因?yàn)橥鶙@锩鎵喝肓怂膫€(gè)元素,所以 BUILD_TUPLE 后面的 oparg 是 4,表示構(gòu)建長(zhǎng)度為 4 的元組。

case TARGET(BUILD_TUPLE){
    PyObject *tup = PyTuple_New(oparg);
    if (tup == NULL)
        goto error;
    // 元素從棧頂?shù)綏5滓来问?nbsp;a、b、c、d
    // 所以元素彈出也是這個(gè)順序
    // 但是注意循環(huán),元素是從后往前設(shè)置的
    // 所以 item[3], item[2], item[1], item[0] = a, b, c, d
    while (--oparg >= 0) {
        PyObject *item = POP();
        PyTuple_SET_ITEM(tup, oparg, item);
    }
    // 將元組 item 壓入棧中,元組為 (d, c, b, a)
    PUSH(tup);
    DISPATCH();
}

此時(shí)棧里面只有一個(gè)元素,指向一個(gè)元組。接下來(lái)是 UNPACK_SEQUENCE,負(fù)責(zé)對(duì)序列進(jìn)行解包,它的指令參數(shù)也是 4,表示要解包的序列的長(zhǎng)度為 4,我們來(lái)看看它的邏輯。

case TARGET(UNPACK_SEQUENCE){
    PREDICTED(UNPACK_SEQUENCE);
    // seq:從棧里面彈出的元組 (d, c, b, a)
    // item:用于遍歷元素
    // items:指向一個(gè) PyObject * 類型的數(shù)組
    PyObject *seq = POP(), *item, **items;
    if (PyTuple_CheckExact(seq) &&
        PyTuple_GET_SIZE(seq) == oparg) {
        // 獲取元組內(nèi)部的 ob_item 成員
        // 元素就存儲(chǔ)在它指向的數(shù)組中
        items = ((PyTupleObject *)seq)->ob_item;
        // 遍歷內(nèi)部的每一個(gè)元素,并依次壓入棧中
        // 由于是從后往前遍歷的,所以遍歷的元素依次是 a b c d
        // 但在壓入棧中之后,元素從棧頂?shù)綏5拙妥兂闪?nbsp;d c b a
        while (oparg--) {
            item = items[oparg];
            Py_INCREF(item);
            PUSH(item);
        }
    } else if (PyList_CheckExact(seq) &&
               PyList_GET_SIZE(seq) == oparg) {
        // 該指令同樣適用于列表,邏輯一樣
        items = ((PyListObject *)seq)->ob_item;
        while (oparg--) {
            item = items[oparg];
            Py_INCREF(item);
            PUSH(item);
        }
    } 
    // ...
    Py_DECREF(seq);
    DISPATCH();
}

最后 STORE_NAME 將 d c b a 依次彈出,賦值給變量 a b c d,從而完成變量交換。所以當(dāng)交換的變量多了之后,不會(huì)直接在運(yùn)行時(shí)棧里面操作,而是將棧里面的元素挨個(gè)彈出,構(gòu)建元組;然后再按照指定順序,將元組里面的元素重新壓到棧里面。

假設(shè)變量 a b c d 的值分別為 1 2 3 4,我們畫圖來(lái)描述一下整個(gè)過(guò)程。

不管是哪一種做法,Python在進(jìn)行變量交換時(shí)所做的事情是不變的,核心分為三步走。首先將等號(hào)右邊的變量,按照從左往右的順序,依次壓入棧中;然后對(duì)運(yùn)行時(shí)棧里面元素的順序進(jìn)行調(diào)整;最后再將運(yùn)行時(shí)棧里面的元素挨個(gè)彈出,還是按照從左往右的順序,再依次賦值給等號(hào)左邊的變量。

只不過(guò)當(dāng)變量不多時(shí),調(diào)整元素位置會(huì)直接基于棧進(jìn)行操作;而當(dāng)達(dá)到四個(gè)時(shí),則需要額外借助于元組。

然后多元賦值也是同理,比如 a, b, c = 1, 2, 3,看一下它的字節(jié)碼。

  0 LOAD_CONST               0 ((123))
  2 UNPACK_SEQUENCE          3
  4 STORE_NAME               0 (a)
  6 STORE_NAME               1 (b)
  8 STORE_NAME               2 (c)

元組直接作為一個(gè)常量被加載進(jìn)來(lái)了,然后解包,再依次賦值。

4)a, b, c, d = d, c, b, a 和 a, b, c, d = [d, c, b, a] 有區(qū)別嗎?

答案是沒(méi)有區(qū)別,兩者在反編譯之后對(duì)應(yīng)的字節(jié)碼指令只有一處不同。

  0 LOAD_NAME                0 (d)
  2 LOAD_NAME                1 (c)
  4 LOAD_NAME                2 (b)
  6 LOAD_NAME                3 (a)
  8 BUILD_LIST               4
 10 UNPACK_SEQUENCE          4
 12 STORE_NAME               3 (a)
 14 STORE_NAME               2 (b)
 16 STORE_NAME               1 (c)
 18 STORE_NAME               0 (d) 

前者是 BUILD_TUPLE,現(xiàn)在變成了 BUILD_LIST,其它部分一模一樣,所以兩者的效果是相同的。當(dāng)然啦,由于元組的構(gòu)建比列表快一些,因此還是推薦第一種寫法。

5)a = b = c = 123 背后的原理是什么?

如果變量 a、b、c 指向的值相同,比如都是 123,那么便可以通過(guò)這種方式進(jìn)行鏈?zhǔn)劫x值。那么它背后是怎么做的呢?

  0 LOAD_CONST               0 (123)
  2 DUP_TOP
  4 STORE_NAME               0 (a)
  6 DUP_TOP
  8 STORE_NAME               1 (b)
 10 STORE_NAME               2 (c)

出現(xiàn)了一個(gè)新的字節(jié)碼指令 DUP_TOP,只要搞清楚它的作用,事情就簡(jiǎn)單了。

case TARGET(DUP_TOP){
    // 獲取棧頂元素,注意是獲取、不是彈出
    // TOP:查看元素,POP:彈出元素
    PyObject *top = TOP();
    // 增加指向?qū)ο蟮囊糜?jì)數(shù)
    Py_INCREF(top);
    // 壓入棧中
    PUSH(top);
    FAST_DISPATCH();
}

所以 DUP_TOP 干的事情就是將棧頂元素拷貝一份,再重新壓到棧里面。另外不管鏈?zhǔn)劫x值語(yǔ)句中有多少個(gè)變量,模式都是一樣的.

我們以 a = b = c = d = e = 123 為例:

   0 LOAD_CONST               0 (123)
   2 DUP_TOP
   4 STORE_NAME               0 (a)
   6 DUP_TOP
   8 STORE_NAME               1 (b)
  10 DUP_TOP
  12 STORE_NAME               2 (c)
  14 DUP_TOP
  16 STORE_NAME               3 (d)
  18 STORE_NAME               4 (e)

將常量壓入運(yùn)行時(shí)棧,然后拷貝一份,賦值給 a;再拷貝一份,賦值給 b;再拷貝一份,賦值給 c;再拷貝一份,賦值給 d;最后自身賦值給 e。

以上就是鏈?zhǔn)劫x值的秘密,其實(shí)沒(méi)有什么好神奇的,就是將棧頂元素進(jìn)行拷貝,再依次賦值。

但是這背后有一個(gè)坑,就是給變量賦的值不能是可變對(duì)象,否則容易造成 BUG。

a = b = c = {}

a["ping"] = "pong"
print(a)  # {'ping': 'pong'}
print(b)  # {'ping': 'pong'}
print(c)  # {'ping': 'pong'}

雖然 Python 一些皆對(duì)象,但對(duì)象都是通過(guò)指針來(lái)間接操作的。所以 DUP_TOP 是將字典的地址拷貝一份,而字典只有一個(gè),因此最終 a、b、c 會(huì)指向同一個(gè)字典。

6)a is b 和 a == b 的區(qū)別是什么?

is 用于判斷兩個(gè)變量是不是引用同一個(gè)對(duì)象,也就是保存的對(duì)象的地址是否相等;而 == 則是判斷兩個(gè)變量引用的對(duì)象是否相等,等價(jià)于 a.__eq__(b) 。

Python 的變量在 C 看來(lái)只是一個(gè)指針,因此兩個(gè)變量是否指向同一個(gè)對(duì)象,等價(jià)于 C 中的兩個(gè)指針存儲(chǔ)的地址是否相等;

而 Python 的 ==,則需要調(diào)用 PyObject_RichCompare,來(lái)比較它們指向的對(duì)象所維護(hù)的值是否相等。

這兩個(gè)語(yǔ)句的字節(jié)碼指令是一樣的,唯一的區(qū)別就是指令 COMPARE_OP 的參數(shù)不同。

  // a is b 
  0 LOAD_NAME                0 (a)
  2 LOAD_NAME                1 (b)
  4 COMPARE_OP               8 (is)
  6 POP_TOP
  
  // a == b
  0 LOAD_NAME                0 (a)
  2 LOAD_NAME                1 (b)
  4 COMPARE_OP               2 (==)
  6 POP_TOP

我們看到指令參數(shù)一個(gè)是 8、一個(gè)是 2,然后是 COMPARE_OP 指令的背后邏輯:

case TARGET(COMPARE_OP){
    // 彈出棧頂元素,這里是 b
    PyObject *right = POP();
    // 顯然 left 就是 a
    // b 被彈出之后,它成為新的棧頂
    PyObject *left = TOP();
    // 進(jìn)行比較,比較結(jié)果為 res
    PyObject *res = cmp_outcome(tstate, oparg, left, right);
    // 減少 left 和 right 引用計(jì)數(shù)
    Py_DECREF(left);
    Py_DECREF(right);
    // 將棧頂元素替換為 res
    SET_TOP(res);
    if (res == NULL)
        goto error;
    // 指令預(yù)測(cè),暫時(shí)不用管
    PREDICT(POP_JUMP_IF_FALSE);
    PREDICT(POP_JUMP_IF_TRUE);
    // 相當(dāng)于 continue
    DISPATCH();
}

所以邏輯很簡(jiǎn)單,核心就在 cmp_outcome 函數(shù)中。

static PyObject *
cmp_outcome(int op, PyObject *v, PyObject *w)
{  
    int res = 0;
    // op 就是 COMPARE_OP 指令的參數(shù)
    switch (op) {
    // PyCmp_IS 是一個(gè)枚舉變量,等于 8
    // 定義在 Include/opcode.h 中
    case PyCmp_IS:
        // is 操作符,在 C 的層面直接一個(gè) == 判斷即可
        res = (v == w);
        break;
    // ...
    default:
        // 而 PyObject_RichCompare 是一個(gè)函數(shù)調(diào)用
        // 比較對(duì)象維護(hù)的值是否相等
        return PyObject_RichCompare(v, w, op);
    }
    v = res ? Py_True : Py_False;
    Py_INCREF(v);
    return v;
}

我們實(shí)際舉個(gè)栗子:

a = 3.14
b = float("3.14")
print(a is b)  # False
print(a == b)  # True

a 和 b 都是 3.14,兩者是相等的,但不是同一個(gè)對(duì)象。

反過(guò)來(lái)也是如此,如果 a is b 成立,那么 a == b 也不一定成立??赡苡腥撕闷?,a is b 成立說(shuō)明 a 和 b 指向的是同一個(gè)對(duì)象,那么 a == b 表示該對(duì)象和自己進(jìn)行比較,結(jié)果應(yīng)該始終是相等的呀,為啥也不一定成立呢?以下面兩種情況為例:

class Girl:

    def __eq__(self, other):
        return False

g = Girl()
print(g is g)  # True
print(g == g)  # False

__eq__ 返回 False,此時(shí)雖然是同一個(gè)對(duì)象,但是兩者不相等。

import math
import numpy as np

a = float("nan")
b = math.nan
c = np.nan

print(a is a, a == a)  # True False
print(b is b, b == b)  # True False
print(c is c, c == c)  # True False

nan 是一個(gè)特殊的浮點(diǎn)數(shù),意思是 not a number(不是一個(gè)數(shù)字),用于表示空值。而 nan 和所有數(shù)字的比較結(jié)果均為 False,即使是和它自身比較。

但需要注意的是,在使用 == 進(jìn)行比較的時(shí)候雖然是不相等的,但如果放到容器里面就不一定了。舉個(gè)例子:

import numpy as np

lst = [np.nan, np.nan, np.nan]
print(lst[0] == np.nan)  # False
print(lst[1] == np.nan)  # False
print(lst[2] == np.nan)  # False
# lst 里面的三個(gè)元素和 np.nan 均不相等

# 但是 np.nan 位于列表中,并且數(shù)量是 3
print(np.nan in lst)  # True
print(lst.count(np.nan))  # 3

出現(xiàn)以上結(jié)果的原因就在于,元素被放到了容器里,而容器的一些 API 在比較元素時(shí)會(huì)先判定它們存儲(chǔ)的對(duì)象的地址是否相同,即:是否指向了同一個(gè)對(duì)象。如果是,直接認(rèn)為相等;否則,再去比較對(duì)象維護(hù)的值是否相等。

可以理解為先進(jìn)行 is 判斷,如果結(jié)果為 True,直接判定兩者相等;如果 is 操作的結(jié)果不為 True,再去進(jìn)行 == 判斷。

因此 np.nan in lst 的結(jié)果為 True,lst.count(np.nan) 的結(jié)果是 3,因?yàn)樗鼈儠?huì)先比較對(duì)象的地址。地址相同,則直接認(rèn)為對(duì)象相等。

在用 pandas 做數(shù)據(jù)處理的時(shí)候,nan 是一個(gè)非常容易坑的地方。

提到 is 和 ==,那么問(wèn)題來(lái)了,在和 True、False、None 比較時(shí),是用 is 還是用 == 呢?

由于 True、False、None 它們不僅是關(guān)鍵字,而且也被看做是一個(gè)常量,最重要的是它們都是單例的,所以我們應(yīng)該用 is 判斷。

另外 is 在底層只需要一個(gè) == 即可完成;但 Python 的 ==,在底層則需要調(diào)用 PyObject_RichCompare 函數(shù)。因此 is 在速度上也更有優(yōu)勢(shì),== 操作肯定比函數(shù)調(diào)用要快。


小結(jié)

以上我們就研究了虛擬機(jī)是如何執(zhí)行字節(jié)碼的,相信你對(duì) Python 虛擬機(jī)也有了更深的了解。說(shuō)白了虛擬機(jī)就是把自己當(dāng)成一顆 CPU,在棧幀中不停地執(zhí)行字節(jié)碼指令。

而執(zhí)行邏輯就是 _PyEval_EvalFrameDefault 里面的那個(gè)大大的 for 循環(huán),for 循環(huán)里面有一個(gè)巨型 switch,case 了所有的分支,不同的分支執(zhí)行不同的邏輯。

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

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多

    国产伦精品一区二区三区精品视频| 亚洲欧美日韩在线中文字幕| 国产视频一区二区三区四区| 丝袜美女诱惑在线观看| 丁香六月啪啪激情综合区| 美国女大兵激情豪放视频播放| 99久久免费看国产精品| 高清亚洲精品中文字幕乱码| 激情爱爱一区二区三区| 国产精品亚洲一区二区| 人妻久久这里只有精品| 欧美日韩国产自拍亚洲| 久久大香蕉一区二区三区| 高清亚洲精品中文字幕乱码| 日本久久中文字幕免费| 五月婷日韩中文字幕四虎| 亚洲二区欧美一区二区| 欧美日韩一区二区综合| 国产色第一区不卡高清| 亚洲天堂国产精品久久精品| 美女激情免费在线观看| 国产麻豆一区二区三区在| 国产精品大秀视频日韩精品| 日韩黄片大全免费在线看| 日本加勒比中文在线观看| 婷婷一区二区三区四区| 亚洲午夜精品视频观看| 国产午夜福利在线免费观看| 精品国产一区二区欧美| 欧美精品中文字幕亚洲| 色婷婷国产熟妇人妻露脸| 久久精品亚洲精品一区| 99视频精品免费视频| 精品亚洲av一区二区三区| 99久久精品午夜一区二| 精品国产亚洲免费91| 国产又粗又爽又猛又黄的| 视频一区日韩经典中文字幕| 高清不卡视频在线观看| 亚洲性日韩精品一区二区| 日韩成人h视频在线观看|