在本篇博客中并村,我們將討論 Python 中 for 循環(huán)的原理屹蚊。?
我們將從一組基本例子和它的語法開始签财,還將討論與 for 循環(huán)關(guān)聯(lián)的 else 代碼塊的用處。?
然后我們將介紹迭代對(duì)象、迭代器和迭代器協(xié)議皮壁,還會(huì)學(xué)習(xí)如何創(chuàng)建自己的迭代對(duì)象和迭代器撩荣。?
之后铣揉,我們將討論如何使用迭代對(duì)象和迭代器實(shí)現(xiàn) for 循環(huán)饶深,以及利用 while 循環(huán)通過迭代器協(xié)議實(shí)現(xiàn) for 循環(huán)邏輯。?
最后逛拱,我們將反編譯一個(gè)簡(jiǎn)單的 for 循環(huán)敌厘,并逐步介紹 Python 解釋器在執(zhí)行 for 循環(huán)時(shí)執(zhí)行的指令,以滿足大家的好奇心橘券。這些有助于理解 for 循環(huán)運(yùn)行時(shí)的內(nèi)部工作原理额湘。?
Python的for循環(huán)?
for 語句是 Python 中執(zhí)行迭代的兩個(gè)語句之一,另一個(gè)語句是 while旁舰。如果你對(duì) Python 的迭代并不是很熟悉的話锋华,Python中的迭代:for、while箭窜、break毯焕、以及continue語句是一個(gè)不錯(cuò)的切入點(diǎn)。?
Python 中磺樱,for 循環(huán)用于遍歷一個(gè)迭代對(duì)象的所有元素纳猫。循環(huán)內(nèi)的語句段會(huì)針對(duì)迭代對(duì)象的每一個(gè)元素項(xiàng)目都執(zhí)行一次。暫且可以將迭代對(duì)象想象成一個(gè)對(duì)象集合竹捉,我們可以一個(gè)個(gè)遍歷里面的元素芜辕。我們將在下一節(jié)對(duì)迭代器和迭代對(duì)象作詳細(xì)說明。?
一個(gè)簡(jiǎn)單的 for 循環(huán)?
我們先從一個(gè)簡(jiǎn)單 for 循環(huán)開始块差,它遍歷一個(gè)字符串列表并打印每一個(gè)字符串侵续。?
如你所見,這個(gè)循環(huán)實(shí)際上遍歷了列表中的每一個(gè)單詞并打印它們憨闰。也就是說状蜗,在循環(huán)的每一次遍歷中,變量 <i>word</i> 都被指定為列表中的一個(gè)元素鹉动,然后執(zhí)行 for 語句中的代碼塊轧坎。由于列表是一個(gè)有序的元素序列,所以循環(huán)也是以相同的順序遍歷這些元素泽示。?
帶有 else 子句的 for 循環(huán)?
Python 中的 for 循環(huán)可以選擇是否關(guān)聯(lián)一個(gè) else 子句缸血。else 子句中的代碼塊是在 for 循環(huán)完成后才開始執(zhí)行的,即在迭代對(duì)象中的所有元素都遍歷完畢之后”吡穑現(xiàn)在我們看一下如何擴(kuò)展前面的示例以包含一個(gè) else 條件(子句)属百。?
else 子句適用于何時(shí)??
你已經(jīng)注意到变姨,else 子句是在 for 循環(huán)完成之后才執(zhí)行的族扰。那么 else 代碼塊的意義是什么呢?for 循環(huán)之后的語句不是也是同樣會(huì)執(zhí)行嗎??
我們很多時(shí)候會(huì)遇到這樣一種情況渔呵,當(dāng)滿足某種條件時(shí)怒竿,中途結(jié)束 for 循環(huán)。且如果這個(gè)條件一直未滿足扩氢,則希望執(zhí)行另一組語句耕驰。我們通常使用布爾類型的標(biāo)記實(shí)現(xiàn),下面是一個(gè)例子录豺。?
調(diào)用結(jié)果:?
而用 else 代碼塊的話朦肘,我們可以避免使用布爾類型的標(biāo)記found_item。我們看看如何使用 else 子句重寫上面的方法双饥。注意如果 for 循環(huán)中的 break 語句被觸發(fā)執(zhí)行媒抠,那么則會(huì)跳過 else 塊。?
所以 else 代碼塊適用于 for 循環(huán)中有 break 語句的情況咏花,且我們希望 break 條件沒有被觸發(fā)的時(shí)候執(zhí)行一些語句趴生。?
否則,與 else 關(guān)聯(lián)的語句只會(huì)在 for 循環(huán)結(jié)束時(shí)才執(zhí)行昏翰。本文的最后一節(jié)查看反編譯的字節(jié)碼時(shí)你會(huì)看到這一點(diǎn)苍匆。?
for 循環(huán)語法?
我們已經(jīng)看到了一些簡(jiǎn)單的例子,接下來以 for 循環(huán)的語法結(jié)束本節(jié)棚菊。?
基本上浸踩,對(duì)于 iterable 中的每一個(gè)元素,都會(huì)執(zhí)行 set_of_statements_1统求。一旦所有的元素都迭代一遍民轴,控制器將跳轉(zhuǎn)到 else 代碼塊中執(zhí)行 set_of_statements_2。?
注意球订,else 子句是可選的。如果沒有發(fā)現(xiàn) else 子句瑰钮,循環(huán)會(huì)在所有元素都遍歷完成后結(jié)束冒滩,并且控制器會(huì)轉(zhuǎn)向程序之后的語句。?
可迭代對(duì)象與迭代器?
可迭代對(duì)象?
在上一節(jié)浪谴,我們使用術(shù)語 iterable 來表示循環(huán)中被迭代的對(duì)象】現(xiàn)在我們來試著了解一下 Python 中的 iterable 對(duì)象是什么。?
Python 中苟耻,一個(gè) iterable 對(duì)象指在 for 循環(huán)中可以被迭代的任意對(duì)象篇恒。這意味著,當(dāng)這個(gè)對(duì)象作為參數(shù)傳遞給 iter()方法時(shí)應(yīng)該返回一個(gè)迭代器凶杖。我們來看一下 Python 中的一些常用的內(nèi)置迭代的例子胁艰。?
如你所見,當(dāng)我們對(duì)一個(gè) iterable 對(duì)象調(diào)用 iter() 時(shí),它會(huì)返回一個(gè)迭代器對(duì)象腾么。?
迭代器?
那么什么是迭代器呢奈梳?迭代器在 Python 中被定義為一個(gè)表現(xiàn)為流式數(shù)據(jù)的對(duì)象〗馐基本上攘须,如果我們將對(duì)象傳遞給內(nèi)置的next() 方法,它應(yīng)該從與之關(guān)聯(lián)的流式數(shù)據(jù)中返回下一個(gè)值殴泰。一旦所有的元素都遍歷結(jié)束于宙,它會(huì)拋出一個(gè)*StopIteration* 異常。next()方法的后續(xù)調(diào)用也都會(huì)拋出*StopIteration* 異常悍汛。?
我們用一個(gè)列表來試一下捞魁。?
迭代器也是可迭代對(duì)象!但是...?
有一個(gè)很有趣的事需要記一下员凝,迭代器同樣支持(強(qiáng)制要求支持迭代器協(xié)議)iter() 方法署驻。這意味著我們可以對(duì)一個(gè)迭代器調(diào)用iter() 方法并獲取它自身的迭代器對(duì)象。?
因此健霹,我們可以在任何期望使用迭代器的地方使用它旺上。比如,for 循環(huán)糖埋。?
然而要注意一點(diǎn)宣吱,在像 list 這樣的容器對(duì)象上調(diào)用 iter() 每次都會(huì)返回不同的迭代器,而在迭代器上調(diào)用 iter() 僅僅返回同一個(gè)迭代器瞳别。?
所以如果你需要進(jìn)行多次迭代征候,并且用迭代器替換普通容器或可迭代對(duì)象,那么第二次你會(huì)看到一個(gè)空的容器祟敛。?
對(duì)一個(gè)列表迭代兩次?
請(qǐng)注意疤坝,這是按照我們的期望運(yùn)行的。?
對(duì)一個(gè)列表迭代器迭代兩次?
請(qǐng)注意馆铁,迭代器在第一次循環(huán)的時(shí)候就已經(jīng)結(jié)束了跑揉,第二次我們看到的是一個(gè)空容器。?
迭代器協(xié)議?
前文我們看到了:?
1. 一個(gè)可迭代對(duì)象埠巨,作為參數(shù)傳遞給 iter() 方法時(shí)返回一個(gè)迭代器历谍。?
2. 一個(gè)迭代器,?
1. 作為參數(shù)傳遞給next()方法時(shí)返回它的下一個(gè)元素或者在所有元素都遍歷結(jié)束時(shí)拋? 出StopIteration 異常辣垒。?
2. 作為參數(shù)傳遞給iter() 方法時(shí)返回它自身望侈。?
迭代協(xié)議僅僅只是一種將對(duì)象定義為迭代器的標(biāo)準(zhǔn)方式。我們已經(jīng)在前一節(jié)看到了這種協(xié)議的實(shí)際應(yīng)用勋桶。根據(jù)協(xié)議脱衙,迭代器應(yīng)該定義以下兩個(gè)方法:?
1. __next__()?
? ?1. 每次調(diào)用這個(gè)方法時(shí)侥猬,應(yīng)該返回迭代器的下一個(gè)元素。一旦元素都遍歷結(jié)束岂丘,它應(yīng)該拋出StopIteration 異常陵究。?
2. 當(dāng)我們調(diào)動(dòng)內(nèi)置函數(shù)next() 時(shí),實(shí)際內(nèi)部調(diào)用的是本方法奥帘。?
2. __iter__()?
? ? 1. 這個(gè)方法返回迭代器自身?
2. 當(dāng)我們調(diào)動(dòng)內(nèi)置函數(shù)iter() 時(shí)铜邮,實(shí)際內(nèi)部調(diào)用的是本方法。?
自己寫一個(gè)迭代器?
現(xiàn)在我們已經(jīng)知道迭代協(xié)議的原理寨蹋,可以寫一個(gè)自己的迭代器了松蒜。我們先看一個(gè)例子,下面我們創(chuàng)建了一個(gè)根據(jù)給定范圍和步長(zhǎng)的 Range 類已旧。?
我們看一下它在 for 循環(huán)中是怎么工作的秸苗。?
注意,Range 類的實(shí)例是迭代器也是可迭代對(duì)象运褪。?
自己寫一個(gè)可迭代對(duì)象?
我們還可以基于 Range 迭代器另外創(chuàng)建一個(gè)可迭代對(duì)象惊楼。它的作用是每當(dāng)調(diào)用 __iter()__ 方法是返回一個(gè)新的迭代器,在這里秸讹,它應(yīng)該返回一個(gè)新的 Range 對(duì)象檀咙。?
在 for 循環(huán)中使用我們這個(gè) RangeIterable。?
for 循環(huán)工作原理?
現(xiàn)在我們已經(jīng)知道什么是迭代器和可迭代對(duì)象璃诀,接下來了解一下 for 循環(huán)是如何工作的弧可。?
再看一下前面的例子。?
當(dāng)我們執(zhí)行上面的代碼塊時(shí)劣欢,發(fā)生了以下這些事情:?
1. 在 for 語句內(nèi)部對(duì)列表 ["You", "are", "awesome!"] 調(diào)用了 iter() 方法棕诵,返回結(jié)果是一個(gè)迭代器。?
2. 然后對(duì)迭代器調(diào)用 next() 方法凿将,并將其返回值賦給變量 word校套。?
3. 之后,會(huì)執(zhí)行 for 循環(huán)中關(guān)聯(lián)的語句塊牧抵。這個(gè)例子中是打印 word搔确。?
4. 在 next() 方法拋出 StopIteration 之前會(huì)一直重復(fù)執(zhí)行第 2,3 步灭忠。?
5. 一旦 next() 拋出 StopIteration,控制器會(huì)跳轉(zhuǎn)到 else 子句(如果存在)并執(zhí)行與 else 關(guān)聯(lián)的語句塊座硕。?
注意:如果在步驟 3 中弛作,for 循環(huán)語句遇到了 break 語句,則跳過 else 代碼塊华匾。?
使用 while 語句實(shí)現(xiàn) for 循環(huán)邏輯?
我們可以像下面這樣使用 while 語句實(shí)現(xiàn)之前的邏輯映琳。?
while 循環(huán)的行為實(shí)際上與 for 循環(huán)相同机隙,上面的代碼會(huì)有以下輸出。?
反編譯 for 循環(huán)?
在本節(jié)萨西,我們將反編譯 for 循環(huán)并逐步說明解釋器在執(zhí)行 for 循環(huán)時(shí)的指令有鹿。這里使用dis 模塊來反編譯 for 循環(huán)。詳細(xì)來說谎脯,就是我們將使用 dis.dis 方法來生成可讀性更高的字節(jié)碼葱跋。?
我們會(huì)使用之前一直用的簡(jiǎn)單 for 循環(huán)示例。接下來將文件寫入文件 for_loop.py源梭。?
我們可以調(diào)用 dis.dis 方法獲得可讀性高的字節(jié)碼娱俺。在終端上運(yùn)行以下命令。?
反編譯輸出的每列表示以下內(nèi)容:?
1.? 第 1 列:代碼行數(shù)废麻。?
2. 第 2 列:如果是跳轉(zhuǎn)指令荠卷,則有 ">>" 符號(hào)。?
3. 第 3 列:以字節(jié)為單位的字節(jié)碼偏移量烛愧。?
4. 第 4 列:字節(jié)碼指令本身油宜。?
5. 第 5 列:展示指令的參數(shù)。如果括號(hào)中有內(nèi)容怜姿,它只是對(duì)參數(shù)做了更好的可讀性轉(zhuǎn)化慎冤。?
現(xiàn)在我們來一步步瀏覽反編譯后的字節(jié)碼,并嘗試了解實(shí)際發(fā)生了什么社牲。?
1. 第 1 行粪薛,即,"for word in [“You”, “are”, “awesome!”]:" 轉(zhuǎn)譯為:?
0 SETUP_LOOP 28 (to 30)?
該語句將 for 循環(huán)中的代碼塊推送到棧中搏恤。這段代碼塊會(huì)跨越 28 個(gè)字節(jié)违寿,達(dá)到 "30"。?
這意味著熟空,如果 for 循環(huán)中有 break 語句藤巢,那么控制器將跳轉(zhuǎn)到偏移位置 "30"。注意當(dāng)遇到 break 語句時(shí)是如何跳過 else 代碼塊的息罗。?
2 LOAD_CONST 0 ((‘You’, ‘a(chǎn)re’, ‘a(chǎn)wesome!’))?
接下來掂咒,列表被推送到棧頂(TOS,之后使用 TOS 表示棧頂或棧頂元素)迈喉。?
4 GET_ITER?
該指令實(shí)現(xiàn) "TOS = iter(TOS)"绍刮。這表示從列表獲取一個(gè)迭代器(當(dāng)前為 TOS),然后將迭代器推送給 TOS挨摸。?
6 FOR_ITER 12 (to 20)?
該指令獲取 TOS孩革,作為當(dāng)前的迭代器, 并調(diào)用 next() 方法得运。?
如果 next() 方法產(chǎn)生一個(gè)值膝蜈,則將其作為 TOS 推送到棧锅移,并執(zhí)行嚇一跳指令 "8 STORE_NAME"。?
一旦 next() 表明迭代器已經(jīng)遍歷結(jié)束(即拋出 StopIteration 異常)饱搏,TOS(迭代器)將從棧中彈出非剃,字節(jié)碼計(jì)數(shù)器會(huì)增加 12。這表示控制器跳轉(zhuǎn)到指令 "20 POP_BLOCK"推沸。?
8 STORE_NAME 0 (word)?
這個(gè)指令執(zhí)行了轉(zhuǎn)換 word = TOS备绽,即,next()返回的值被賦給變量word坤学。?
2. 第 1 行疯坤,即,"print(word)" 轉(zhuǎn)譯為:?
10 LOAD_NAME 1 (print)?
將可調(diào)用方法print 推送到棧中深浮。?
12 LOAD_NAME 0 (word)?
將棧中的word作為參數(shù)推送給print压怠。?
14 CALL_FUNCTION 1?
調(diào)用帶位置參數(shù)的函數(shù)。?
像我們看到的指令那樣飞苇,與函數(shù)關(guān)聯(lián)的參數(shù)會(huì)出現(xiàn)在 TOS 中菌瘫。在獲得可調(diào)用象的對(duì)(如print)之前,會(huì)彈出所有遇到的參數(shù)布卡。?
一旦獲得可調(diào)用對(duì)象雨让,則把所有參數(shù)傳遞給它并調(diào)用。?
可調(diào)用對(duì)象執(zhí)行結(jié)束后忿等,把返回值推送到 TOS 中栖忠,這里是 None。?
16 POP_TOP?
TOS(棧頂元素)贸街,即將函數(shù)的返回值從棧中移除(彈出)庵寞。?
18 JUMP_ABSOLUTE 6?
此時(shí)字節(jié)碼計(jì)數(shù)器為 “6”,這表示下一條指令將執(zhí)行 "6 FOR_ITER"薛匪。這是循環(huán)遍歷迭代器中元素的方式捐川。?
注意,一旦迭代器中的元素都遍歷結(jié)束逸尖,指令 "6 FOR_ITER" 會(huì)結(jié)束循環(huán)并跳轉(zhuǎn)到 "20 POP_BLOCK"古沥。?
20 POP_BLOCK?
POP_BLOCK 會(huì)從代碼塊的棧中移除由 “0 SETUP_LOOP” 設(shè)置的代碼塊。?
3. 注意第 3 行(對(duì)應(yīng)else)娇跟,沒有關(guān)聯(lián)任何特殊指令岩齿。程序控制器會(huì)順序執(zhí)行下一條與else 相關(guān)的指令。?
4. 第 4 行苞俘,即纯衍,"print("See you later!")" 轉(zhuǎn)譯為:?
22 LOAD_NAME 1 (print)?
推送與print 相關(guān)的可調(diào)用方法到棧中。?
24 LOAD_CONST 1 ('See you later!')?
推送可調(diào)用函數(shù)的參數(shù)對(duì)象到棧中苗胀。?
26 CALL_FUNCTION 1?
可調(diào)用函數(shù)及其參數(shù)會(huì)從棧中彈出襟诸,然后執(zhí)行函數(shù)并將其返回值推送到 TOS。?
28 POP_TOP?
TOS(棧頂元素)基协,即將函數(shù)返回值(這里是 None)從棧中移除歌亲。?
5. 下面的兩個(gè)指令只是簡(jiǎn)單的將腳本的返回值(None)加載到棧并返回。?
30 LOAD_CONST 2 (None)?
32 RETURN_VALUE?
喔澜驮!現(xiàn)在我們已經(jīng)了解了 for 循環(huán)反編譯后的指令陷揪。希望這有助于更好地理解 for 循環(huán)的工作原理。