這篇文章是對 500 Lines or Less 一書中高效爬蟲一章的部分翻譯,原文在此 -> How Python Generators Work。建議結合《流暢的 Python》食用。
在掌握 Python 生成器之前,你必須瞭解常規 Python 函式的工作原理。通常,當一個 Python 函式呼叫子程式(subroutine)時,這個子程式將一直持有控制權,只有當子程式結束(返回或者丟擲異常)後,控制權才還給呼叫者:
1 2 3 4 5 |
>>> def foo(): ... bar() ... >>> def bar(): ... pass |
標準的 Python 直譯器是用 C 寫的。直譯器用一個叫做 PyEval_EvalFrameEx 的 C 函式來執行 Python 函式。它接受一個 Python 的堆疊幀(stack frame)物件,並在這個堆疊幀的上下文中執行 Python 位元組碼。這是 foo 的位元組碼:
1 2 3 4 5 6 7 |
>>> import dis >>> dis.dis(foo) 2 0 LOAD_GLOBAL 0 (bar) 3 CALL_FUNCTION 0 (0 positional, 0 keyword pair) 6 POP_TOP 7 LOAD_CONST 0 (None) 10 RETURN_VALUE |
foo 函式將 bar 載入到堆疊中並呼叫它,然後從堆疊中彈出返回值,最後載入並返回 None。
當 PyEval_EvalFrameEx 遇到 CALL_FUNCTION 位元組碼的時候,它會建立一個新的 Python 堆疊幀,然後用這個新的幀作為引數遞迴呼叫 PyEval_EvalFrameEx 來執行 bar。
Python 的堆疊幀是分配在堆記憶體中的,理解這一點非常重要!Python 直譯器是個普通的 C 程式,所以它的堆疊幀就是普通的堆疊。但是它操作的 Python 堆疊幀是在堆上的。除了其他驚喜之外,這意味著 Python 的堆疊幀可以在它的呼叫之外存活。(FIXME: 可以在它呼叫結束後存活)。要以互動方式檢視,請從 bar 內儲存當前幀:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
>>> import inspect >>> frame = None >>> def foo(): ... bar() ... >>> def bar(): ... global frame ... frame = inspect.currentframe() ... >>> foo() >>> # The frame was executing the code for 'bar'. >>> frame.f_code.co_name 'bar' >>> # Its back pointer refers to t >>> def bar(): ... global frame ... frame = inspect.currentframe()he frame for 'foo'. >>> caller_frame = frame.f_back >>> caller_frame.f_code.co_name 'foo' |
現在這項技術被用到了 Python 生成器(generator)上——使用程式碼物件和堆疊幀這些相同的元件來產生奇妙的效果。
這是一個生成器函式(generator function):
1 2 3 4 5 6 7 |
>>> def gen_fn(): ... result = yield 1 ... print('result of yield: {}'.format(result)) ... result2 = yield 2 ... print('result of 2nd yield: {}'.format(result2)) ... return 'done' ... |
當 Python 將 gen_fn 編譯為位元組碼時,它會看到 yield 語句,然後知道 gen_fn 是個生成器函式,而不是普通函式。它會設定一個標誌來記住這個事實:
1 2 3 4 |
>>> # The generator flag is bit position 5. >>> generator_bit = 1 << 5 >>> bool(gen_fn.__code__.co_flags & generator_bit) True |
當你呼叫一個生成器函式時,Python 會看到生成器標誌,實際上並不執行該函式,而是建立一個生成器(generator):
1 2 3 |
>>> gen = gen_fn() >>> type(gen) <class 'generator'> |
Python 生成器封裝了一個堆疊幀和一個對生成器函式程式碼的引用,在這裡就是對 gen_fn 函式體的引用:
1 2 |
>>> gen.gi_code.co_name 'gen_fn' |
呼叫 gen_fn 產生的所有生成器都指向同一個程式碼物件,但是每個都有自己的堆疊幀。這個堆疊幀並不存在於實際的堆疊上,它在堆記憶體上等待著被使用
堆疊幀有個 “last instruction”(FIXME: translate this or not?) 指標,指向最近執行的那條指令。剛開始的時候 last instruction 指標是 -1,意味著生成器尚未開始:
1 2 |
>>> gen.gi_frame.f_lasti -1 |
當我們呼叫 send 時,生成器達到第一個 yield 處然後暫停執行。send 的返回值是 1,這是因為 gen 把 1 傳給了 yield 表示式:
1 2 |
>>> gen.send(None) 1 |
現在生成器的指令指標(instruction pointer)向前移動了 3 個位元組碼,這些是編譯好的 56 位元組的 Python 程式碼的一部分:
1 2 3 4 |
>>> gen.gi_frame.f_lasti 3 >>> len(gen.gi_code.co_code) 56 |
生成器可以在任何時候被任何函式恢復執行,因為它的堆疊幀實際上不在堆疊上——它在堆(記憶體)上。生成器在呼叫呼叫層次結構中的位置不是固定的,它不需要遵循常規函式執行時遵循的先進後出順序。生成器被是被解放了的,它像雲一樣浮動。
我們可以將 “hello” 傳送到這個生成器中,它會成為 yield 表示式的值,然後生成器會繼續執行,直到產出(yield)了 2:
1 2 3 |
>>> gen.send('hello') result of yield: hello 2 |
現在這個生成器的堆疊幀包含區域性變數 result:
1 2 |
>>> gen.gi_frame.f_locals {'result': 'hello'} |
從 gen_fn 建立的其他生成器將具有自己的堆疊幀和區域性變數。
當我們再次呼叫 send 時,生成器將從它第二個 yield 處繼續執行,然後以產生特殊異常 StopIteration 結束:
1 2 3 4 5 |
>>> gen.send('goodbye') result of 2nd yield: goodbye Traceback (most recent call last): File "<input>", line 1, in <module> StopIteration: done |
異常有一個值,它是那個生成器的返回值:字串 “done”。