前言
這篇文章大部分來自 David Beazley 在 PyCon 2014 的 PPT 《Generators: The Final Frontier》。這個PPT很長而且非常燒腦,建議在閱讀前應瞭解 Python 的生成器與攜程相關知識,推薦《流暢的 Python》。
生成器(generator)
使用 yield
來定義一個生成器
def countdown(n):
while n > 0:
yield n
n -= 1
c = countdown(10)
c
<generator object countdown at 0x0000021F5EAB9F10>
生成器可用於迭代
for x in countdown(10):
print('倒數:', x)
倒數: 10
倒數: 9
倒數: 8
倒數: 7
倒數: 6
倒數: 5
倒數: 4
倒數: 3
倒數: 2
倒數: 1
可以透過 next()
來一步步地讓生成器 yield
一個值,直到函式迭代器結束並丟擲 StopIteration
。如果你對這一頭霧水,建議閱讀《Fluent Python》14.4 章。
這裡 for
其實相當於不斷地呼叫 next
並處理 StopIteration
c = countdown(3)
# next(c)
3
# next(c)
2
# next(c)
1
把生成器當作管道
你可以巢狀生成器,這會導致類似於 Unix 命令列管道的效果
def add_A(seq):
for item in seq:
yield item + '-A'
def add_B(seq):
for item in seq:
yield item + '-B'
def add_C(seq):
for item in seq:
yield item + '-C'
seq = ['apple', 'banana', 'orange']
stacked_generator = add_C(add_B(add_A(seq)))
for item in stacked_generator:
print(item)
apple-A-B-C
banana-A-B-C
orange-A-B-C
可以看到,我們為 seq
裡的每項都依次新增了一個 tag。
yield 可以接受傳值
yield
的作用是向呼叫者返回一個值,呼叫者其實也可以向生成器傳值。
def receiver():
while True:
received_item = yield
print('收到:', received_item)
def caller():
recv = receiver()
next(recv) # 使生成器前進到 yield
for i in 'abcd':
recv.send(i)
caller()
收到: a
收到: b
收到: c
收到: d
那 send
函式的返回值是什麼呢?
def receiver():
call_times = 0
while True:
item = yield call_times
print('收到:', item)
call_times += 1
def caller():
recv = receiver()
next(recv)
for i in 'abcd':
ret_value = recv.send(i)
print('返回值: ', ret_value)
caller()
收到: a
返回值: 1
收到: b
返回值: 2
收到: c
返回值: 3
收到: d
返回值: 4
所以 send
可以向生成器傳值的同時會讓生成器前進到下一個 yield
語句,並將 yield
右側的值作為返回值。
生成器 101
yield
用於定義生成器函式- 只要
yield
存在該函式必定是一個生成器 - 呼叫該函式返回一個生成器
讓一個生成器前進
使用 next
使一個生成器前進到下一個 yield
語句處,並將產出值(yielded value)作為其返回值。使用 gen.__next__()
效果相同。
注意:這是一個新建立的生成器唯一允許的操作。
def generator():
yield 'a'
yield 'b'
gen = generator()
# next(gen)
'a'
# next(gen)
'b'
給生成器傳值
可以透過呼叫生成器的 send
方法來向生成器傳值,這將讓生成器從上次暫停的 yield
前進到下個 yield
,並將產出值作為 send
的返回值。
def generator():
item = yield 'a'
print(item)
another_item = yield 'b'
gen = generator()
print(next(gen))
print(gen.send(1))
a
1
b
關閉一個生成器
透過呼叫生成器 close
方法可以生成器在 yield
語句處丟擲 GeneratorExit
。這時僅允許 return
,如果沒有捕捉這個錯誤,生成器會靜默關閉,不丟擲錯誤。
def generator():
times = 0
while True:
yield times
times += 1
gen = generator()
print(next(gen))
print(next(gen))
gen.close() # 不會丟擲錯誤
0
1
丟擲錯誤
呼叫生成器的 throw
方法可以在 yield
處丟擲某個特定型別的錯誤,如果生成器內部可以捕捉這個錯誤,那生成器將前進到下個 yield
語句處,並將產出值作為 throw
的返回值,否則中止生成器。throw
的函式簽名如下:
throw(typ, [,val, [,tb]])
其中 tyb
是某錯誤型別,val
是錯誤資訊,tb
為 traceback。更多資訊可以參考官方的PEP0342
def generator():
try:
yield 'apple'
except RuntimeError as e:
print('捕捉到:', e)
yield 'banana'
gen = generator()
print(next(gen))
print(gen.throw(RuntimeError, '執行錯誤'))
apple
捕捉到: 執行錯誤
banana
生成器返回值
如果在生成器函式中加上 return
那在執行到 return
時將會把返回值作為 StopIteration
的值傳遞出去。這個是 Python3 的特性,Python2 生成器不能返回某個值。
def generator():
yield
return 'apple'
g = generator()
next(g)
try:
next(g)
except StopIteration as e:
print(e)
apple
生成器委託
使用 yield from
可以幫你對一個生成器不斷呼叫 next
函式,並返回生成器的返回值。言下之意是你可以在生成器裡呼叫生成器。
def generator():
yield 'a'
yield 'b'
return 'c'
def func():
result = yield from generator()
print(result)
呼叫 func
結果是返回一個生成器
# func()
<generator object func at 0x0000021F5EB0F990>
# next(func())
'a'
另外一個例子
def chain(x, y):
yield from x
yield from y
a = [1, 2, 3]
b = [4, 5, 6]
for i in chain(a, b):
print(i, end=' ')
1 2 3 4 5 6
c = [7, 8, 9]
for i in chain(a, chain(b, c)):
print(i, end=' ')
1 2 3 4 5 6 7 8 9
Part 1總結
生成器定義
def generator():
...
yield
...
return result
生成器操作
gen = generator()
# 使一個生成器前進
next(gen)
# 傳遞一個值
gen.send(item)
# 中止生成器
gen.close()
# 丟擲錯誤
gen.throw(exc, val, tb)
# 委託
result = yield from gen
本作品採用《CC 協議》,轉載必須註明作者和本文連結