1. 程式人生 > >Python Generator

Python Generator

Python 中的生成器 (generator) 是一個十分有用的工具,它讓我們能方便地生成迭代器 (iterator)。這篇文章裡,我們就來說說什麼是生成器,生成器有什麼作用以及如何使用。

本文需要你對 Python 基本的語法有一定的瞭解,並知道 iterator 是什麼,且我們可以通過 next(iterator) 來獲取 iterator 的下一個值。

#iterator 簡介

想象這樣一個需求,我們需要從網上獲取一些圖片,這些圖片的名字的規律是數字遞增,因此我們有類似下面的程式碼:

def get_images(n):
result = []
for i in range(n):

result.append(get_image_by_id(i))
return result

images = get_images(n)

現在,假設我們需要對圖片進行一些操作,但依當前圖片的情況不同,我們也許不需要後續的圖片,並且, get_image_by_id 是一個很耗時的操作,我們希望在不需要的情況下儘量避免呼叫它。

換句話說,我們希望能對 get_image_by_id 進行懶執行 (lazy evalution)。這也不難,我們可以這麼做:

image_id = -1
def next_image():
global image_id
image_id += 1

return get_image_by_id(image_id)

image0 = next_image()
image1 = next_image()

這裡函式 next_image 使用了全域性的變數儲存當前已獲取的圖片的 id,使用全域性變數決定了 next_image 無法被兩個個體使用。例如兩個人都想從頭獲取圖片,這是沒法完成的,因此我們定義一個類來解決這個問題:

class ImageRepository:
def __init__(self):
self.image_id = -1
def next_image(self):
self.image_id += 1

return get_image_by_id(self.image_id)

repo = ImageRepository()
image0 = repo.next_image()
image1 = repo.next_image()

如果你熟悉 iterator 的話,應該知道上面這個需求是一個典型的 iterator,因此我們可以實現 __iter____next__ 方法來將它變成一個 iterator,從而充分利用 iterator 現成的一些工具:

class ImageRepository:
def __init__(self):
self.image_id = -1
def __iter__(self):
return self
def __next__(self):
self.image_id += 1
return get_image_by_id(self.image_id)

for image in ImageRepository():
# some operation on each image

是不是也沒什麼難度?下面我們看看其它的一些思路。

#從 Iterator 到 Generator

上面的 iterator 的例子有一個特點,就是它需要我們自己去管理 iterator 的狀態,即 image_id。這種寫法跟我們的思維差異較大,因此懶惰的我們希望有一些更好,更方便的寫法,這就是我們要介紹的 genrator 。

在 Python 中,只要一個函式中使用了 yeild 這個關鍵字,就代表這個函式是一個生成器 (generator)。而 yield 的作用就相當於讓 Python 幫我們把一個“序列”的邏輯轉換成 iterator 的形式。例如,上面的例子用 generator 的語法寫就變成了:

def image_repository()
image_id = -1
while True:
image_id += 1
yield get_image_by_id(image_id)

for image in image_repository():
# do some operation

首先,就寫法上,這種寫法與我們最先開始的迴圈寫法最為類似;其次,在功能上,呼叫這個函式 image_repository() 返回的是一個 generator object,它實現了 iterator 的方法,因此可以將它作為普通的 iterator 使用 (for ... in ...);最後,注意到我們所要做的,就是把平時使用的 return 換成 yield 就可以了。

再舉個例子:

def fibonacci():
a, b = (0, 1)
while True:
yield a
a, b = b, a+b

fibos = fibonacci()
next(fibos) #=> 0
next(fibos) #=> 1
next(fibos) #=> 1
next(fibos) #=> 2

通過 generator ,我們很輕鬆地就寫出了一個無限的斐波那契數列函式。如果要手寫的話,它相當於:

class Fibonacci():
def __init__(self):
self.a, self.b = (0, 1)
def __iter__(self):
return self
def __next__(self):
result = self.a
self.a, self.b = self.b, self.a + self.b
return result

fibos = Fibonacci()
next(fibos) #=> 0
next(fibos) #=> 1
next(fibos) #=> 1
next(fibos) #=> 2

顯然 generator 的寫法更為清晰,且符合我們平時書寫順序結構的習慣。

#Generator 與控制流

前面我們提到,Generator 的作用其實是實現了懶執行 (lazy evalution) ,即在真正需要某個值的時候才真正去計算這個值。因此,更進一步,Generator 其實是返回了控制流。當一個 generator 執行到 yeild 語句時,它便儲存當前的狀態,返回所給的結果(也可以沒有),並將當前的執行流還給呼叫它的函式,而當再次呼叫它時,Generator 就從上次 yield 的位置繼續執行。例如:

def generator():
print('before')
yield # break 1
print('middle')
yield # break 2
print('after')

x = generator()
next(x)
#=> before
next(x)
#=> middle
next(x)
#=> after
#=> exception StopIteration

可以看到,第一次呼叫 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:
def call_this_first():
pass

def call_this_second():
pass

def call_this_last():
pass

def api():
first()
yield
second()
yield
last()

通過這種方式提供的 API 能有效防止使用者的誤用。這也是 generator 能 “從控制流中返回” 這個特性的一個應用。

#yield 加強版

上面我們說到 Generator 允許我們暫停控制流,並返回一些資料,之後能從暫停的位置恢復。那我們就會有疑問,既然暫停控制流時能返回資料,那恢復控制流的時候能不能傳遞資料到暫停的位置呢? PEP 342 中就加入了相關的支援。這個需求說起來比較抽象,我們舉個例子:

想象我們要寫一個函式,計算多個數的平均值,我們稱它為 averager。我們希望每次呼叫都提供一個新的數,並返回至今為止所有提供的數的平均值。讓我們先來看看用 generator 的加強版語法怎麼實現:

def averager():
sum = 0
num = 0
while True:
sum += (yield sum / num if num > 0 else 0)
num += 1

x = averager()
x.send(None)
#=> 0
x.send(1)
#=> 1.0
x.send(2)
#=> 1.5
x.send(3)
#=> 2.0

這個加強版的語法是這麼工作的: 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:
def __init__(self):
self.sum = 0
self.num = 0
def avg_num(self, n):
self.sum += n
self.num += 1
return self.sum / self.num
averager = Averager()
averager.avg_num(1)
#=> 1.0
averager.avg_num(2)
#=> 1.5
averager.avg_num(3)
#=> 2.0

這種寫法比 generator 更直觀,並且使用者呼叫起來也方便,不需要額外呼叫一次 x.send(None)。顯然 generator 的加強版語法並不是為了專門用來解決我們這裡提到的需求的。它要解決的真正問題是支援協程 (coroutine) 來實現非同步程式設計的。由於這個問題比較複雜,這裡就不深入討論了。

#yield from

考慮我們有多個 generator 並想把 generator 組合起來,如:

def odds(n):
for i in range(n):
if i % 2 == 1:
yield i

def evens(n):
for i in range(n):
if i % 2 == 0:
yield i

def odd_even(n):
for x in odds(n):
yield x
for x in evens(n):
yield x

for x in odd_even(6):
print(x)
#=> 1, 3, 5, 0, 2, 4

for x in generator(): yield x 這種寫法不太方便,因此 PEP 380 引入了 yield from 語法,來替代我們前面說的這種語法,因此上面的例子可以改成:

def odd_even(n):
yield from odds(n)
yield from evens(n)

是不是清晰許多?

#小結

我們簡單介紹了 iterator ;之後介紹了使用 generator 來更方便地生成 iterator;之後舉例說明了 yield 的加強版語法,最後介紹了 yield from 語法。

  1. 當一個函式裡使用了 yield 關鍵字,則該函式就被稱為一個 generator (生成器)。
  2. Generator 被呼叫時返回 Generator Object,它實現了 iterator 的介面。所以可以認為 generator 呼叫後返回了一個 iterator。
  3. yeild 可以從控制流中暫時退出,之後可以從退出的位置恢復。通過加強版的語法還能在恢復時傳遞一些值給 generator。
  4. yield from 語法可以用來方便地組合不同的 generator。

Generator 是生成 iterator 非常方便的工具,希望本文能讓你對 generator 有更好的瞭解,也希望 Generator 能給你今後的 Python 生涯帶來更多的方便。