Python 生成器原理詳解

發表於2017-10-12

這篇文章是對 500 Lines or Less 一書中高效爬蟲一章的部分翻譯,原文在此 -> How Python Generators Work。建議結合《流暢的 Python》食用。

在掌握 Python 生成器之前,你必須瞭解常規 Python 函式的工作原理。通常,當一個 Python 函式呼叫子程式(subroutine)時,這個子程式將一直持有控制權,只有當子程式結束(返回或者丟擲異常)後,控制權才還給呼叫者:

標準的 Python 直譯器是用 C 寫的。直譯器用一個叫做 PyEval_EvalFrameEx 的 C 函式來執行 Python 函式。它接受一個 Python 的堆疊幀(stack frame)物件,並在這個堆疊幀的上下文中執行 Python 位元組碼。這是 foo 的位元組碼:

foo 函式將 bar 載入到堆疊中並呼叫它,然後從堆疊中彈出返回值,最後載入並返回 None

PyEval_EvalFrameEx 遇到 CALL_FUNCTION 位元組碼的時候,它會建立一個新的 Python 堆疊幀,然後用這個新的幀作為引數遞迴呼叫 PyEval_EvalFrameEx 來執行 bar

Python 的堆疊幀是分配在堆記憶體中的,理解這一點非常重要!Python 直譯器是個普通的 C 程式,所以它的堆疊幀就是普通的堆疊。但是它操作的 Python 堆疊幀是在堆上的。除了其他驚喜之外,這意味著 Python 的堆疊幀可以在它的呼叫之外存活。(FIXME: 可以在它呼叫結束後存活)。要以互動方式檢視,請從 bar 內儲存當前幀:

607595308-59c695a90838c_articlex

現在這項技術被用到了 Python 生成器(generator)上——使用程式碼物件和堆疊幀這些相同的元件來產生奇妙的效果。

這是一個生成器函式(generator function):

當 Python 將 gen_fn 編譯為位元組碼時,它會看到 yield 語句,然後知道 gen_fn 是個生成器函式,而不是普通函式。它會設定一個標誌來記住這個事實:

當你呼叫一個生成器函式時,Python 會看到生成器標誌,實際上並不執行該函式,而是建立一個生成器(generator):

Python 生成器封裝了一個堆疊幀和一個對生成器函式程式碼的引用,在這裡就是對 gen_fn 函式體的引用:

呼叫 gen_fn 產生的所有生成器都指向同一個程式碼物件,但是每個都有自己的堆疊幀。這個堆疊幀並不存在於實際的堆疊上,它在堆記憶體上等待著被使用607595308-59c695a90838c_articlex
堆疊幀有個 “last instruction”(FIXME: translate this or not?) 指標,指向最近執行的那條指令。剛開始的時候 last instruction 指標是 -1,意味著生成器尚未開始:

當我們呼叫 send 時,生成器達到第一個 yield 處然後暫停執行。send 的返回值是 1,這是因為 gen 把 1 傳給了 yield 表示式:

現在生成器的指令指標(instruction pointer)向前移動了 3 個位元組碼,這些是編譯好的 56 位元組的 Python 程式碼的一部分:

生成器可以在任何時候被任何函式恢復執行,因為它的堆疊幀實際上不在堆疊上——它在堆(記憶體)上。生成器在呼叫呼叫層次結構中的位置不是固定的,它不需要遵循常規函式執行時遵循的先進後出順序。生成器被是被解放了的,它像雲一樣浮動。

我們可以將 “hello” 傳送到這個生成器中,它會成為 yield 表示式的值,然後生成器會繼續執行,直到產出(yield)了 2:

現在這個生成器的堆疊幀包含區域性變數 result

gen_fn 建立的其他生成器將具有自己的堆疊幀和區域性變數。

當我們再次呼叫 send 時,生成器將從它第二個 yield 處繼續執行,然後以產生特殊異常 StopIteration 結束:

異常有一個值,它是那個生成器的返回值:字串 “done”。

相關文章