偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

流程控制語句 For、While 是怎么實(shí)現(xiàn)的?

開發(fā) 前端
for 循環(huán)遍歷可迭代對(duì)象時(shí),會(huì)先拿到對(duì)應(yīng)的迭代器,那如果遍歷的就是一個(gè)迭代器呢?答案是依舊調(diào)用 __iter__,只不過由于本身就是一個(gè)迭代器,所以返回的還是其本身。

楔子

在介紹 if 語句的時(shí)候,我們看到了最基本的控制流,其核心就是跳轉(zhuǎn)。但無論是 if 還是 match,都只能向前跳轉(zhuǎn)。而接下來介紹的 for、while 循環(huán),指令是可以回退的,也就是向后跳轉(zhuǎn)。

另外在 if 語句的分支中,無論哪個(gè)分支,其指令的跳躍距離都是當(dāng)前指令與目標(biāo)指令的距離,相當(dāng)于向前跳了多少步。那么指令回退時(shí),是不是相當(dāng)于向后跳了多少步呢?帶著疑問,我們開始今天的內(nèi)容。

for 控制流

我們看一個(gè)簡(jiǎn)單的 for 循環(huán)的字節(jié)碼。

import dis

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

dis.dis(compile(code_string, "<file>", "exec"))

反編譯之后,字節(jié)碼指令如下。

0 RESUME                   0
      # 加載常量 1,壓入運(yùn)行時(shí)棧
      2 LOAD_CONST               0 (1)
      # 加載常量 2,壓入運(yùn)行時(shí)棧
      4 LOAD_CONST               1 (2)
      # 將運(yùn)行時(shí)棧的元素彈出,構(gòu)建長(zhǎng)度為 2 的列表,并壓入棧中
      6 BUILD_LIST               2
      # 將上一步構(gòu)建的列表從棧頂彈出,并用符號(hào) lst 與之綁定
      # 到此 lst = [1, 2] 便完成了
      8 STORE_NAME               0 (lst)
      
      # 從全局名字空間中加載 lst
     10 LOAD_NAME                0 (lst)
      # 獲取對(duì)應(yīng)的迭代器,即 iter(lst)
     12 GET_ITER
      # 開始 for 循環(huán),將里面的元素依次迭代出來
      # 如果循環(huán)結(jié)束,跳轉(zhuǎn)到偏移量為 38 的指令,即 END_FOR
>>   14 FOR_ITER                10 (to 38)
      # 用符號(hào) item 和迭代出的元素進(jìn)行綁定
     18 STORE_NAME               1 (item)

     20 PUSH_NULL
      # 對(duì)應(yīng) print(item)
     22 LOAD_NAME                2 (print)
     24 LOAD_NAME                1 (item)
     26 CALL                     1
     34 POP_TOP
      # 到此,一次遍歷就結(jié)束了,那么向后跳轉(zhuǎn) 12 個(gè)指令
      # 會(huì)來到偏移量為 14 的指令,進(jìn)行下一次遍歷
     36 JUMP_BACKWARD           12 (to 14)
      # 循環(huán)結(jié)束
>>   38 END_FOR
     40 RETURN_CONST             2 (None)

我們直接從 10 GET_ITER 開始看起,首先 for 循環(huán)遍歷的對(duì)象必須是可迭代對(duì)象,然后會(huì)調(diào)用它的 __iter__ 方法,得到迭代器。再不斷地調(diào)用迭代器的 __next__ 方法,一步一步將里面的值全部迭代出來,當(dāng)出現(xiàn) StopIteration 異常時(shí),for 循環(huán)捕捉,最后退出。

另外,我們說 Python 里面是先有值,后有變量,for 循環(huán)也不例外。循環(huán)的時(shí)候,先將迭代器中的元素迭代出來,然后再讓變量 item 指向。

因此包含 10 個(gè)元素的迭代器,需要迭代 11 次才能結(jié)束。因?yàn)?for 循環(huán)事先是不知道迭代 10 次就能結(jié)束的,它需要再迭代一次,發(fā)現(xiàn)沒有元素可以迭代、并捕獲拋出的 StopIteration 之后,才能結(jié)束。

for 循環(huán)遍歷可迭代對(duì)象時(shí),會(huì)先拿到對(duì)應(yīng)的迭代器,那如果遍歷的就是一個(gè)迭代器呢?答案是依舊調(diào)用 __iter__,只不過由于本身就是一個(gè)迭代器,所以返回的還是其本身。

將元素迭代出來之后,就開始執(zhí)行 for 循環(huán)體的邏輯了。

執(zhí)行完之后,通過 JUMP_BACKWARD 跳轉(zhuǎn)到字節(jié)碼偏移量為 14、也就是 FOR_ITER 的位置開始下一次循環(huán)。這里我們發(fā)現(xiàn)它沒有跳到 GET_ITER 那里,所以可以得出結(jié)論,for 循環(huán)在遍歷的時(shí)候只會(huì)創(chuàng)建一次迭代器。

下面來看指令對(duì)應(yīng)的具體邏輯:

TARGET(GET_ITER) {
    // 獲取棧頂元素,即上一步壓入的列表指針
    PyObject *iterable = stack_pointer[-1];
    PyObject *iter;
    #line 2255 "Python/bytecodes.c"
    // 調(diào)用 PyObject_GetIter,獲取對(duì)應(yīng)的迭代器
    // 這個(gè)函數(shù)在介紹迭代器的時(shí)候已經(jīng)說過了
    // 等價(jià)于 iter = type(iterable).__iter__(iterable)
    iter = PyObject_GetIter(iterable);
    #line 3216 "Python/generated_cases.c.h"
    Py_DECREF(iterable);
    #line 2258 "Python/bytecodes.c"
    if (iter == NULL) goto pop_1_error;
    #line 3220 "Python/generated_cases.c.h"
    // 將迭代器 iter 設(shè)置為棧頂元素
    stack_pointer[-1] = iter;
    DISPATCH();
}

當(dāng)創(chuàng)建完迭代器之后,就正式進(jìn)入 for 循環(huán)了。所以從 FOR_ITER 開始,進(jìn)入了虛擬機(jī)層面上的 for 循環(huán)。

源代碼中的 for 循環(huán),在虛擬機(jī)層面也一定對(duì)應(yīng)著一個(gè)相應(yīng)的循環(huán)控制結(jié)構(gòu)。因?yàn)闊o論進(jìn)行怎樣的變換,都不可能在虛擬機(jī)層面利用順序結(jié)構(gòu)來實(shí)現(xiàn)源碼層面上的循環(huán)結(jié)構(gòu),這也可以看作是程序的拓?fù)洳蛔冃浴?/p>

因此源代碼是宏觀的,虛擬機(jī)執(zhí)行字節(jié)碼是微觀的,盡管兩者的層級(jí)不同,但本質(zhì)上等價(jià)的,是程序從一種形式到另一種形式的等價(jià)轉(zhuǎn)換。

我們來看一下 FOR_ITER 指令對(duì)應(yīng)的具體實(shí)現(xiàn):

TARGET(FOR_ITER) {
    // ...
    // 從棧頂獲取迭代器對(duì)象(指針)
    PyObject *iter = stack_pointer[-1];
    PyObject *next;
    #line 2304 "Python/bytecodes.c"
    // ...
    // 調(diào)用迭代器類型對(duì)象的 tp_iternext,將迭代器內(nèi)的元素迭代出來
    next = (*Py_TYPE(iter)->tp_iternext)(iter);
    // 如果 next 為 NULL,說明迭代出現(xiàn)異常
    if (next == NULL) {
        // 如果異常還不是 StopIteration,那么跳轉(zhuǎn)到 error 標(biāo)簽
        if (_PyErr_Occurred(tstate)) {
            if (!_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) {
                goto error;
            }
            monitor_raise(tstate, frame, next_instr-1);
            _PyErr_Clear(tstate);
        }
        // 否則說明是 StopIteration,那么證明迭代完畢
        Py_DECREF(iter);
        STACK_SHRINK(1);
        /* Jump forward oparg, then skip following END_FOR instruction */
        // 跳轉(zhuǎn)到 END_FOR 標(biāo)簽
        JUMPBY(INLINE_CACHE_ENTRIES_FOR_ITER + oparg + 1);
        DISPATCH();
    }
    #line 3297 "Python/generated_cases.c.h"
    // 到這里說明 next != NULL,證明迭代出元素了,那么壓入運(yùn)行時(shí)棧
    STACK_GROW(1);
    stack_pointer[-1] = next;
    next_instr += 1;
    DISPATCH();
}

在將迭代出來的元素壓入運(yùn)行時(shí)棧之后,會(huì)執(zhí)行 STORE_NAME。然后虛擬機(jī)將沿著字節(jié)碼指令的順序一條一條地執(zhí)行下去,從而完成輸出的動(dòng)作。

但是我們知道,for 循環(huán)中肯定會(huì)有指令回退的動(dòng)作。從字節(jié)碼中也看到了,for 循環(huán)遍歷一次之后,會(huì)再次跳轉(zhuǎn)到 FOR_ITER,而跳轉(zhuǎn)所使用的指令就是 JUMP_BACKWARD。

TARGET(JUMP_BACKWARD) {
    PREDICTED(JUMP_BACKWARD);
    #line 2151 "Python/bytecodes.c"
    assert(oparg < INSTR_OFFSET());
    JUMPBY(-oparg);
    #line 3033 "Python/generated_cases.c.h"
    CHECK_EVAL_BREAKER();
    DISPATCH();
}

我們看到它調(diào)用了 JUMPBY,這個(gè)宏在介紹 if 語句的時(shí)候說過。

// Python/ceval_macros.h

// 從字節(jié)碼的起始位置向前跳轉(zhuǎn) x 個(gè)指令
#define JUMPTO(x)       (next_instr = _PyCode_CODE(frame->f_code) + (x))
// 從 next_instr 處(指向當(dāng)前待執(zhí)行的指令)向前跳轉(zhuǎn) x 個(gè)指令
#define JUMPBY(x)       (next_instr += (x))

因?yàn)閰?shù)是 -oparg,所以相當(dāng)于向后跳轉(zhuǎn)了 oparg 個(gè)指令,從而實(shí)現(xiàn)指令回退,繼續(xù)下一輪循環(huán)。

但天下沒有不散的宴席,隨著迭代的進(jìn)行,for 循環(huán)總有退出的那一刻,而這個(gè)退出的動(dòng)作只能落在 FOR_ITER 的身上。在 FOR_ITER 指令執(zhí)行的過程中,如果遇到了 StopIteration,就意味著迭代結(jié)束了。

這個(gè)結(jié)果將導(dǎo)致虛擬機(jī)會(huì)將迭代器從運(yùn)行時(shí)棧中彈出,同時(shí)執(zhí)行一個(gè) JUMPBY 動(dòng)作,向前跳躍,在字節(jié)碼的層面是向下,也就是偏移量增大的方向。

while 控制流

看完了 for,再來看看 while,并且我們還要分析兩個(gè)關(guān)鍵字:break、continue。

import dis

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

dis.dis(compile(code_string, "<file>", "exec"))

看一下它的指令:

0 RESUME                   0
       
      # a = 0
      2 LOAD_CONST               0 (0)
      4 STORE_NAME               0 (a)
      
      # 比較 a < 10
>>    6 LOAD_NAME                0 (a)
      8 LOAD_CONST               1 (10)
     10 COMPARE_OP               2 (<)
      # 如果 a < 10 為假,說明循環(huán)結(jié)束
      # 跳轉(zhuǎn)到偏移量為 80 的指令
     14 POP_JUMP_IF_FALSE       32 (to 80)
      
      # 到這里說明 while 條件成立,進(jìn)入循環(huán)體
      # 執(zhí)行 a += 1
>>   16 LOAD_NAME                0 (a)
     18 LOAD_CONST               2 (1)
     20 BINARY_OP               13 (+=)
     24 STORE_NAME               0 (a)
      
      # 比較 a == 5
     26 LOAD_NAME                0 (a)
     28 LOAD_CONST               3 (5)
     30 COMPARE_OP              40 (==)
      # 如果 a == 5 為假,跳轉(zhuǎn)到偏移量為 38 的指令
     34 POP_JUMP_IF_FALSE        1 (to 38)
      # 否則說明 a == 5 為真,執(zhí)行 continue
      # 由于 continue 是立即進(jìn)入下一輪循環(huán)
      # 所以向后跳轉(zhuǎn)到偏移量為 6 的指令
      # 所以在虛擬機(jī)的層面,continue 就是一個(gè)跳轉(zhuǎn)指令
     36 JUMP_BACKWARD           16 (to 6)
      
      # 比較 a == 7
>>   38 LOAD_NAME                0 (a)
     40 LOAD_CONST               4 (7)
     42 COMPARE_OP              40 (==)
      # 如果 a == 7 為假,跳轉(zhuǎn)到偏移量為 50 的指令
     46 POP_JUMP_IF_FALSE        1 (to 50)
      # 否則說明 a == 7 為真,執(zhí)行 break
      # 而 break 是跳出循環(huán),并且循環(huán)的下面也沒有代碼了
      # 所以直接 RETURN_CONST
     48 RETURN_CONST             5 (None)
      
      # print(a)
>>   50 PUSH_NULL
     52 LOAD_NAME                1 (print)
     54 LOAD_NAME                0 (a)
     56 CALL                     1
     64 POP_TOP
      
      # print(a) 結(jié)束后應(yīng)該跳轉(zhuǎn)到 while a < 10 處,進(jìn)行下一輪循環(huán)
      # 但這里又執(zhí)行了 a < 10
     66 LOAD_NAME                0 (a)
     68 LOAD_CONST               1 (10)
     70 COMPARE_OP               2 (<)
     74 POP_JUMP_IF_FALSE        1 (to 78)
      # 如果為假,然后跳轉(zhuǎn)到 a += 1 處,因此整體效果和直接跳轉(zhuǎn)到 while 處是等價(jià)的
     76 JUMP_BACKWARD           31 (to 16)
>>   78 RETURN_CONST             5 (None)
>>   80 RETURN_CONST             5 (None)

有了 for 循環(huán),再看 while 循環(huán)就簡(jiǎn)單多了,整體邏輯和 for 高度相似,當(dāng)然里面還結(jié)合了 if。

剛才說了,盡管源代碼和字節(jié)碼的層級(jí)不同,但本質(zhì)上是等價(jià)的,是程序從一種形式到另一種形式的等價(jià)轉(zhuǎn)換。在源碼中能看到的,在字節(jié)碼當(dāng)中也能看到。比如源代碼中的 continue 會(huì)跳轉(zhuǎn)到循環(huán)所在位置,那么在字節(jié)碼中自然也會(huì)對(duì)應(yīng)一個(gè)跳轉(zhuǎn)指令。

小結(jié)

以上我們就探討了 Python 的兩種循環(huán),總的來說沒什么難度,本質(zhì)上還是跳轉(zhuǎn)。只不過相對(duì) if 只能向前跳轉(zhuǎn),循環(huán)還可以向后跳轉(zhuǎn)。

責(zé)任編輯:武曉燕 來源: 古明地覺的編程教室
相關(guān)推薦

2024-11-01 16:05:26

2010-05-11 12:53:58

Unix awk

2009-09-04 10:42:56

C#流程控制語句

2010-07-19 10:11:58

Perl流程控制語句

2015-07-23 15:17:37

JavaScript循環(huán)語句

2011-08-24 16:36:00

T-SQL

2017-05-31 17:09:52

LinuxShell命令

2024-06-06 09:09:41

SQL循環(huán)控制命令

2011-08-23 13:36:11

T-SQL查詢流程控制語句

2016-08-29 20:51:16

awkLinux開源

2009-12-15 09:56:51

Ruby流程控制

2011-09-08 13:53:31

Node.js

2010-03-18 16:37:13

Python 程序流程

2021-05-27 09:30:51

Java流程控制

2020-11-13 10:29:37

流程控制語句

2021-05-27 05:27:22

流程控制Rust

2025-07-01 08:20:00

JUC包Java并發(fā)

2022-07-27 08:31:28

SQL開發(fā)控制

2010-05-11 12:17:51

Unix awk

2021-08-05 06:54:05

流程控制default
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)