理解 Python 的 for 循環

在本篇博客中,我們將討論 Python 中 for 循環的原理。

我們將從一組基本例子和它的語法開始,還將討論與 for 循環關聯的 else 代碼塊的用處。

然後我們將介紹迭代對象、迭代器和迭代器協議,還會學習如何創建自己的迭代對象和迭代器。

之後,我們將討論如何使用迭代對象和迭代器實現 for 循環,以及利用 while 循環通過迭代器協議實現 for 循環邏輯。

最後,我們將反編譯一個簡單的 for 循環,並逐步介紹 Python 解釋器在執行 for 循環時執行的指令,以滿足大家的好奇心。這些有助於理解 for 循環運行時的內部工作原理。

Python的for循環

for 語句是 Python 中執行迭代的兩個語句之一,另一個語句是 while。如果你對 Python 的迭代並不是很熟悉的話,Python中的迭代:for、while、break、以及continue語句是一個不錯的切入點。

Python 中,for 循環用於遍歷一個迭代對象的所有元素。循環內的語句段會針對迭代對象的每一個元素項目都執行一次。暫且可以將

迭代對象想象成一個對象集合,我們可以一個個遍歷裡面的元素。我們將在下一節對迭代器迭代對象作詳細說明。

一個簡單的 for 循環

我們先從一個簡單 for 循環開始,它遍歷一個字符串列表並打印每一個字符串。

理解 Python 的 for 循環

如你所見,這個循環實際上遍歷了列表中的每一個單詞並打印它們。也就是說,在循環的每一次遍歷中,變量 word 都被指定為列表中的一個元素,然後執行 for 語句中的代碼塊。由於列表是一個有序的元素序列,所以循環也是以相同的順序遍歷這些元素。

帶有 else 子句的 for 循環

Python 中的 for 循環可以選擇是否關聯一個 else 子句。else 子句中的代碼塊是在 for 循環完成後才開始執行的,即在迭代對象中的所有元素都遍歷完畢之後。現在我們看一下如何擴展前面的示例以包含一個 else 條件(子句)。

理解 Python 的 for 循環

else 子句適用於何時?

你已經注意到,else 子句是在 for 循環完成之後才執行的。那麼 else 代碼塊的意義是什麼呢?for 循環之後的語句不是也是同樣會執行嗎?

我們很多時候會遇到這樣一種情況,當滿足某種條件時,中途結束 for 循環。且如果這個條件一直未滿足,則希望執行另一組語句。我們通常使用布爾類型的標記實現,下面是一個例子。

理解 Python 的 for 循環

調用結果:

理解 Python 的 for 循環

而用 else 代碼塊的話,我們可以避免使用布爾類型的標記 found_item。我們看看如何使用 else 子句重寫上面的方法。注意如果 for 循環中的 break 語句被觸發執行,那麼則會跳過 else 塊。

理解 Python 的 for 循環

所以 else 代碼塊適用於 for 循環中有 break 語句的情況,且我們希望 break 條件沒有被觸發的時候執行一些語句。

否則,與 else 關聯的語句只會在 for 循環結束時才執行。本文的最後一節查看反編譯的字節碼時你會看到這一點。

for 循環語法

我們已經看到了一些簡單的例子,接下來以 for 循環的語法結束本節。

理解 Python 的 for 循環

基本上,對於 iterable 中的每一個元素,都會執行 set_of_statements_1。一旦所有的元素都迭代一遍,控制器將跳轉到 else 代碼塊中執行 set_of_statements_2

注意,else 子句是可選的。如果沒有發現 else 子句,循環會在所有元素都遍歷完成後結束,並且控制器會轉向程序之後的語句。

可迭代對象與迭代器

可迭代對象

在上一節,我們使用術語 iterable 來表示循環中被迭代的對象。現在我們來試著瞭解一下 Python 中的 iterable 對象是什麼。

Python 中,一個 iterable 對象指在 for 循環中可以被迭代的任意對象。這意味著,當這個對象作為參數傳遞給

iter()方法時應該返回一個迭代器。我們來看一下 Python 中的一些常用的內置迭代的例子。

理解 Python 的 for 循環

如你所見,當我們對一個 iterable 對象調用 iter() 時,它會返回一個迭代器對象。

迭代器

那麼什麼是迭代器呢?迭代器在 Python 中被定義為一個表現為流式數據的對象。基本上,如果我們將對象傳遞給內置的

next() 方法,它應該從與之關聯的流式數據中返回下一個值。一旦所有的元素都遍歷結束,它會拋出一個 *StopIteration* 異常。next() 方法的後續調用也都會拋出 *StopIteration* 異常。

我們用一個列表來試一下。

理解 Python 的 for 循環

迭代器也是可迭代對象!但是...

有一個很有趣的事需要記一下,迭代器同樣支持(強制要求支持迭代器協議iter() 方法。這意味著我們可以對一個迭代器調用 iter() 方法並獲取它自身的迭代器對象。

理解 Python 的 for 循環

因此,我們可以在任何期望使用迭代器的地方使用它。比如,for 循環。

然而要注意一點,在像 list 這樣的容器對象上調用 iter() 每次都會返回不同的迭代器,而在迭代器上調用 iter() 僅僅返回同一個迭代器。

理解 Python 的 for 循環

理解 Python 的 for 循環

所以如果你需要進行多次迭代,並且用迭代器替換普通容器或可迭代對象,那麼第二次你會看到一個空的容器。

對一個列表迭代兩次

請注意,這是按照我們的期望運行的。

理解 Python 的 for 循環

對一個列表迭代器迭代兩次

請注意,迭代器在第一次循環的時候就已經結束了,第二次我們看到的是一個空容器。

理解 Python 的 for 循環

迭代器協議

前文我們看到了:

1. 一個可迭代對象,作為參數傳遞給

iter() 方法時返回一個迭代器

2. 一個迭代器,

1. 作為參數傳遞給 next() 方法時返回它的下一個元素或者在所有元素都遍歷結束時拋 出 StopIteration 異常。

2. 作為參數傳遞給 iter() 方法時返回它自身。

迭代協議僅僅只是一種將對象定義為迭代器的標準方式。我們已經在前一節看到了這種協議的實際應用。根據協議,迭代器應該定義以下兩個方法:

1. __next__()

1. 每次調用這個方法時,應該返回迭代器的下一個元素。一旦元素都遍歷結束,它應該拋出 StopIteration 異常。

2. 當我們調動內置函數 next() 時,實際內部調用的是本方法。

2. __iter__()

1. 這個方法返回迭代器自身

2. 當我們調動內置函數 iter() 時,實際內部調用的是本方法。

自己寫一個迭代器

現在我們已經知道迭代協議的原理,可以寫一個自己的迭代器了。我們先看一個例子,下面我們創建了一個根據給定範圍和步長的 Range 類。

理解 Python 的 for 循環

我們看一下它在 for 循環中是怎麼工作的。

理解 Python 的 for 循環

注意,Range 類的實例是迭代器也是可迭代對象。

自己寫一個可迭代對象

我們還可以基於 Range 迭代器另外創建一個可迭代對象。它的作用是每當調用 __iter()__ 方法是返回一個新的迭代器,在這裡,它應該返回一個新的 Range 對象。

理解 Python 的 for 循環

在 for 循環中使用我們這個 RangeIterable。

理解 Python 的 for 循環

for 循環工作原理

現在我們已經知道什麼是迭代器和可迭代對象,接下來了解一下 for 循環是如何工作的。

再看一下前面的例子。

理解 Python 的 for 循環

當我們執行上面的代碼塊時,發生了以下這些事情:

1. 在 for 語句內部對列表

["You", "are", "awesome!"] 調用了 iter() 方法,返回結果是一個迭代器

2. 然後對迭代器調用 next() 方法,並將其返回值賦給變量 word

3. 之後,會執行 for 循環中關聯的語句塊。這個例子中是打印 word

4. 在 next() 方法拋出 StopIteration 之前會一直重複執行第 2,3 步。

5. 一旦 next() 拋出 StopIteration,控制器會跳轉到 else 子句(如果存在)並執行與 else 關聯的語句塊。

注意:如果在步驟 3 中,for 循環語句遇到了 break 語句,則跳過 else 代碼塊。

使用 while 語句實現 for 循環邏輯

我們可以像下面這樣使用 while 語句實現之前的邏輯。

理解 Python 的 for 循環

while 循環的行為實際上與 for 循環相同,上面的代碼會有以下輸出。

理解 Python 的 for 循環

反編譯 for 循環

在本節,我們將反編譯 for 循環並逐步說明解釋器在執行 for 循環時的指令。這裡使用 dis 模塊來反編譯 for 循環。詳細來說,就是我們將使用 dis.dis 方法來生成可讀性更高的字節碼。

我們會使用之前一直用的簡單 for 循環示例。接下來將文件寫入文件 for_loop.py

理解 Python 的 for 循環

我們可以調用 dis.dis 方法獲得可讀性高的字節碼。在終端上運行以下命令。

理解 Python 的 for 循環

反編譯輸出的每列表示以下內容:

1. 第 1 列:代碼行數。

2. 第 2 列:如果是跳轉指令,則有 ">>" 符號。

3. 第 3 列:以字節為單位的字節碼偏移量。

4. 第 4 列:字節碼指令本身。

5. 第 5 列:展示指令的參數。如果括號中有內容,它只是對參數做了更好的可讀性轉化。

現在我們來一步步瀏覽反編譯後的字節碼,並嘗試瞭解實際發生了什麼。

1. 第 1 行,即,"for word in [“You”, “are”, “awesome!”]:" 轉譯為:

  • 0 SETUP_LOOP 28 (to 30)
  • 該語句將 for 循環中的代碼塊推送到棧中。這段代碼塊會跨越 28 個字節,達到 "30"。

  • 這意味著,如果 for 循環中有 break 語句,那麼控制器將跳轉到偏移位置 "30"。注意當遇到 break 語句時是如何跳過 else 代碼塊的。
  • 2 LOAD_CONST 0 ((‘You’, ‘are’, ‘awesome!’))
  • 接下來,列表被推送到棧頂(TOS,之後使用 TOS 表示棧頂或棧頂元素)。

  • 4 GET_ITER
  • 該指令實現 "TOS = iter(TOS)"。這表示從列表獲取一個迭代器(當前為 TOS),然後將迭代器推送給 TOS。
  • 6 FOR_ITER 12 (to 20)
  • 該指令獲取 TOS,作為當前的迭代器, 並調用 next() 方法。

  • 如果 next() 方法產生一個值,則將其作為 TOS 推送到棧,並執行嚇一跳指令 "8 STORE_NAME"。
  • 一旦 next() 表明迭代器已經遍歷結束(即拋出 StopIteration 異常),TOS(迭代器)將從棧中彈出,字節碼計數器會增加 12。這表示控制器跳轉到指令 "20 POP_BLOCK"。
  • 8 STORE_NAME 0 (word)
  • 這個指令執行了轉換 word = TOS,即,next() 返回的值被賦給變量 word

2. 第 1 行,即,

"print(word)" 轉譯為:

  • 10 LOAD_NAME 1 (print)
  • 將可調用方法 print 推送到棧中。

  • 12 LOAD_NAME 0 (word)
  • 將棧中的 word 作為參數推送給 print

  • 14 CALL_FUNCTION 1
  • 調用帶位置參數的函數。

  • 像我們看到的指令那樣,與函數關聯的參數會出現在 TOS 中。在獲得可調用象的對(如 print)之前,會彈出所有遇到的參數。
  • 一旦獲得可調用對象,則把所有參數傳遞給它並調用。

  • 可調用對象執行結束後,把返回值推送到 TOS 中,這裡是 None。
  • 16 POP_TOP
  • TOS(棧頂元素),即將函數的返回值從棧中移除(彈出)。

  • 18 JUMP_ABSOLUTE 6
  • 此時字節碼計數器為 “6”,這表示下一條指令將執行 "6 FOR_ITER"。這是循環遍歷迭代器中元素的方式。
  • 注意,一旦迭代器中的元素都遍歷結束,指令 "6 FOR_ITER" 會結束循環並跳轉到 "20 POP_BLOCK"。
  • 20 POP_BLOCK
  • POP_BLOCK 會從代碼塊的棧中移除由 “0 SETUP_LOOP” 設置的代碼塊。

3. 注意第 3 行(對應

else),沒有關聯任何特殊指令。程序控制器會順序執行下一條與 else 相關的指令。

4. 第 4 行,即,"print("See you later!")" 轉譯為:

  • 22 LOAD_NAME 1 (print)
  • 推送與 print 相關的可調用方法到棧中。

  • 24 LOAD_CONST 1 ('See you later!')
  • 推送可調用函數的參數對象到棧中。

  • 26 CALL_FUNCTION 1
  • 可調用函數及其參數會從棧中彈出,然後執行函數並將其返回值推送到 TOS。

  • 28 POP_TOP
  • TOS(棧頂元素),即將函數返回值(這裡是 None)從棧中移除。

5. 下面的兩個指令只是簡單的將腳本的返回值(None)加載到棧並返回。

  • 30 LOAD_CONST 2 (None)
  • 32 RETURN_VALUE

喔!現在我們已經瞭解了 for 循環反編譯後的指令。希望這有助於更好地理解 for 循環的工作原理。

總結

通過本篇博客我們學到了以下內容:

1. 用 Python 如何寫 for 循環?

2. 什麼時候適合用帶有 else 子句的 for 循環?

3. 什麼是迭代器和迭代對象?

4. 什麼是迭代協議?

5. 如何自定義一個迭代器和迭代對象?

6. for 循環的工作原理是什麼?

7. 如何使用 while 循環仿寫一個 for 循環?

8. 何如通過 dis 模塊反編譯 for 循環並查看由 Python 解釋器執行的高可讀性指令?如何閱讀並理解反編譯指令?


分享到:


相關文章: