Python Generator
Python 中的生成器 (generator) 是一個十分有用的工具,它讓我們能方便地生成迭代器 (iterator)。這篇文章裡,我們就來說說什麼是生成器,生成器有什麼作用以及如何使用。
本文需要你對 Python 基本的語法有一定的瞭解,並知道 iterator 是什麼,且我們可以通過 next(iterator)
來獲取 iterator
的下一個值。
#iterator 簡介
想象這樣一個需求,我們需要從網上獲取一些圖片,這些圖片的名字的規律是數字遞增,因此我們有類似下面的程式碼:
def get_images(n): |
現在,假設我們需要對圖片進行一些操作,但依當前圖片的情況不同,我們也許不需要後續的圖片,並且, get_image_by_id
是一個很耗時的操作,我們希望在不需要的情況下儘量避免呼叫它。
換句話說,我們希望能對 get_image_by_id
進行懶執行 (lazy evalution)。這也不難,我們可以這麼做:
image_id = -1 |
這裡函式 next_image
使用了全域性的變數儲存當前已獲取的圖片的 id
,使用全域性變數決定了 next_image
無法被兩個個體使用。例如兩個人都想從頭獲取圖片,這是沒法完成的,因此我們定義一個類來解決這個問題:
class ImageRepository: |
如果你熟悉 iterator 的話,應該知道上面這個需求是一個典型的 iterator,因此我們可以實現 __iter__
及 __next__
方法來將它變成一個 iterator,從而充分利用
iterator 現成的一些工具:
class ImageRepository: |
是不是也沒什麼難度?下面我們看看其它的一些思路。
#從 Iterator 到 Generator
上面的 iterator 的例子有一個特點,就是它需要我們自己去管理 iterator 的狀態,即
image_id
。這種寫法跟我們的思維差異較大,因此懶惰的我們希望有一些更好,更方便的寫法,這就是我們要介紹的 genrator 。
在 Python 中,只要一個函式中使用了 yeild
這個關鍵字,就代表這個函式是一個生成器 (generator)。而 yield
的作用就相當於讓 Python 幫我們把一個“序列”的邏輯轉換成 iterator 的形式。例如,上面的例子用 generator 的語法寫就變成了:
def image_repository() |
首先,就寫法上,這種寫法與我們最先開始的迴圈寫法最為類似;其次,在功能上,呼叫這個函式 image_repository()
返回的是一個 generator object,它實現了 iterator 的方法,因此可以將它作為普通的 iterator 使用 (for ... in ...
);最後,注意到我們所要做的,就是把平時使用的 return
換成 yield
就可以了。
再舉個例子:
def fibonacci(): |
通過 generator ,我們很輕鬆地就寫出了一個無限的斐波那契數列函式。如果要手寫的話,它相當於:
class Fibonacci(): |
顯然 generator 的寫法更為清晰,且符合我們平時書寫順序結構的習慣。
#Generator 與控制流
前面我們提到,Generator 的作用其實是實現了懶執行 (lazy evalution) ,即在真正需要某個值的時候才真正去計算這個值。因此,更進一步,Generator 其實是返回了控制流。當一個 generator 執行到 yeild 語句時,它便儲存當前的狀態,返回所給的結果(也可以沒有),並將當前的執行流還給呼叫它的函式,而當再次呼叫它時,Generator 就從上次 yield 的位置繼續執行。例如:
def generator(): |
可以看到,第一次呼叫 next(x)
,程式執行到了 break 1
處就返回了,第二次呼叫
next(x)
時從之前 yield 的位置(即 break 1
) 處繼續執行。同理,第三次呼叫
next(x)
時從 break 2
恢復執行,最終退出函式時,丟擲 StopIteration
異常,代表 generator
已經退出。
為什麼要提到 generator 的“控制流”的特點呢?因為 genrator 表允許我們從“順序”執行流中暫時退出,利用這個特性我們能做一些很有意義的事。
例如,我們提供一個 API,它要求呼叫者首先呼叫 call_this_first
然後做一些操作,然後再呼叫 call_this_second
,再做一些操作,最後呼叫 call_this_last
。也就是說這些 API 的呼叫是有順序的。但 API 的提供者並沒有辦法強制使用者按我們所說的順序去呼叫這幾個 API。但有了 generator,我們可以用另一種形式提供 API,如下:
class API: |
通過這種方式提供的 API 能有效防止使用者的誤用。這也是 generator 能 “從控制流中返回” 這個特性的一個應用。
#yield 加強版
上面我們說到 Generator 允許我們暫停控制流,並返回一些資料,之後能從暫停的位置恢復。那我們就會有疑問,既然暫停控制流時能返回資料,那恢復控制流的時候能不能傳遞資料到暫停的位置呢? PEP 342 中就加入了相關的支援。這個需求說起來比較抽象,我們舉個例子:
想象我們要寫一個函式,計算多個數的平均值,我們稱它為 averager
。我們希望每次呼叫都提供一個新的數,並返回至今為止所有提供的數的平均值。讓我們先來看看用
generator 的加強版語法怎麼實現:
def averager(): |
這個加強版的語法是這麼工作的: yield 之前是語句,現在是表示式,是表示式就意味著我們能這麼寫 x = yield 10
, y = 10 + (yield)
, foo(yield 42)
。Python 規定,除非 yield 左邊直接跟著等號(不準確),否則必須用擴號括起來。
當 Python 執行到 yield 表示式時,它首先計算 yield 右邊的表示式,上例中即為
sum / num if num > 0 else 0
的值,暫停當前的控制流,並返回。之後,除了可以用
next(generator)
的方式(即 iterator 的方式)來恢復控制流之外,還可以通過
generator.send(some_value)
來傳遞一些值。例如上例中,如果我們呼叫
x.send(3)
則 Python 恢復控制流, (yield sum/sum ...)
的值則為我們賦予的
3
,並接著執行 sum += 3
以及之後的語句。注意的是,如果這時我們用的是
next(generator)
則它等價為 generator.send(None)
。
最後要注意的是,剛呼叫 generator 生成 generator object 時,函式並沒有真正執行,也就是說這時控制流並不在 yield
表示式上等待使用者傳遞值,因此我們需要先呼叫 generate.send(None)
或 next(generator)
來觸發最開始的執行。
那麼說到這裡,用 generator 來實現這個需求明顯沒有其它方法好用,例如:
class Averager: |
這種寫法比 generator 更直觀,並且使用者呼叫起來也方便,不需要額外呼叫一次
x.send(None)
。顯然 generator 的加強版語法並不是為了專門用來解決我們這裡提到的需求的。它要解決的真正問題是支援協程 (coroutine) 來實現非同步程式設計的。由於這個問題比較複雜,這裡就不深入討論了。
#yield from
考慮我們有多個 generator 並想把 generator 組合起來,如:
def odds(n): |
for x in generator(): yield x
這種寫法不太方便,因此 PEP
380 引入了 yield from
語法,來替代我們前面說的這種語法,因此上面的例子可以改成:
def odd_even(n): |
是不是清晰許多?
#小結
我們簡單介紹了 iterator ;之後介紹了使用 generator 來更方便地生成 iterator;之後舉例說明了 yield 的加強版語法,最後介紹了 yield from 語法。
- 當一個函式裡使用了 yield 關鍵字,則該函式就被稱為一個 generator (生成器)。
- Generator 被呼叫時返回 Generator Object,它實現了 iterator 的介面。所以可以認為 generator 呼叫後返回了一個 iterator。
- yeild 可以從控制流中暫時退出,之後可以從退出的位置恢復。通過加強版的語法還能在恢復時傳遞一些值給 generator。
- yield from 語法可以用來方便地組合不同的 generator。
Generator 是生成 iterator 非常方便的工具,希望本文能讓你對 generator 有更好的瞭解,也希望 Generator 能給你今後的 Python 生涯帶來更多的方便。