yield 產生器
April 22, 2022你可以在函式中使用 yield
來產生值,表面上看來,yield
有點像是 return
,不過函式並不會因為 yield
而結束,只是將流程控制權讓給函式的呼叫者。
yield 入門
以下來個模仿 range
的實作,自訂一個 xrange
函式:
def xrange(n):
x = 0
while x != n:
yield x
x += 1
for n in xrange(10):
print(n)
就流程來看,xrange
函式首次執行時,使用 yield
產生 x
,然後回到主流程使用 print
顯示該值,接著流程重回 xrange
函式 yield
之後繼續執行,迴圈中再度使用 yield
產生 x
,然後又回到主流程使用 print
顯示該值,這樣的反覆流程,會直到 xrange
的 while
迴圈結束為止。
顯然地,這樣的流程有別於函式中使用了 return
,函式就結束了的情況。實際上,當函式中使用 yield
產生值時,呼叫該函式會傳回 generator
物件,也就是產生器,此物件具有 __next__
方法,通常會使用 next
函式呼叫該方法取出下個產生值(也就是 yield
的值),若無法產生下一個值(也就是含有 yield
的函式結束了),會發生 StopIteration
例外(Exception)。
>>> g = xrange(2)
>>> type(g)
<class 'generator'>
>>> next(g)
0
>>> next(g)
1
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
因此,for in
實際上是對 xrange
傳回的產生器進行迭代,如果一個物件具有 __next__
方法,for in
會呼叫 __next__
方法取得值,並在遇到 StopIteration
時結束迭代。
因為每次呼叫產生器的 __next__
時,產生器才會運算並傳回下個產生值,因此就解釋了先前為何提到產生器,都稱其具有惰性求值的效果。
在〈for Comprehension〉談過,可以使用 ()
包括 for Compherension,這會建立一個 generator
物件,這個物件也可以使用 for in
來迭代。
儘管少見,然而具有 yield
的函式,還是可以使用 return
,由於 return
就是直接結束函式,因而執行 return
時會引發 StopIteration
,被 return
的值可以透過 StopIteration
的 args
來取得。
send 資料
yield
是個運算式,除了可以呼叫產生器的 __next__
方法,取得 yield
右方的值之外,還可以透過 send
方法指定值,令其成為 yield
運算結果,也就是產生器可以給呼叫者值,呼叫者也可以指定值給產生器,這成了一種溝通機制。
例如,設計一個簡單的生產者與消費者程式:
import sys
import random
def producer():
while True:
data = random.randint(0, 9)
print('生產了:', data)
yield data
def consumer():
while True:
data = yield
print('消費了:', data)
def clerk(jobs, producer, consumer):
print('執行 {} 次生產與消費'.format(jobs))
p = producer()
c = consumer()
next(c)
for i in range(jobs):
data = next(p)
c.send(data)
clerk(int(sys.argv[1]), producer, consumer)
由於 send
方法的引數會是 yield
的運算結果,因此 clerk
流程中必須先使用 next
,使得流程首次執行至 consumer
函式中 data = yield
處先執行 yield
,執行 yield
會令流程回到 clerk
函式,之後執行至 next
,這時流程進行至 producer
函式的 yield data
,在 clerk
取得 data
之後,接著執行 c.send(data)
,這時流程回到 consumer
先前 data=yield
處,send
方法的引數此時成為 yield
的結果。一個執行結果如下:
>python producer_consumer.py 3
執行 3 次生產與消費
生產了: 4
消費了: 4
生產了: 5
消費了: 5
生產了: 9
消費了: 9
不過使用 send
,代表又要轉移流程控制權,這種流程交織轉移並不容易控制,因此並不鼓勵使用 send
;類似地,如果想對產生器引發例外,可以使用產生器的 throw
方法,這個方法接受的三個引數為例外類型、實例以及 traceback
物件,然而並不鼓勵使用。
yield from
如果打算建立一個產生器函式,資料來源是直接從另一個產生器取得,那會怎麼樣呢?舉例來說,range
函式就是傳回產生器,而你打算建立一個 np_range
函式,可以產生指定數字的正負範圍,但不包含 0:
def np_range(n):
for i in range(0 - n, 0):
yield i
for i in range(1, n + 1):
yield i
# 顯示[-5, -4, -3, -2, -1, 1, 2, 3, 4, 5]
print(list(np_range(5)))
因為 np_range
必須是個產生器,結果就是得逐一從來源產生器取得資料,再將之 yield
,像是這邊重複使用了 for in
來迭代。
Python 3.3 新增了 yield from
語法,上面的程式片段可以直接改寫為以下實作:
def np_range(n):
yield from range(0 - n, 0)
yield from range(1, n + 1)
# 顯示[-5, -4, -3, -2, -1, 1, 2, 3, 4, 5]
print(list(np_range(5)))
當需要直接從某個產生器取得資料,以便建立另一個產生器時,yield from
可以作為銜接的語法。