第 35 條 不要通過 throw 變換生成器狀態
? 除 yield from 表達式(參見 第 33 條) 與 send 方法(參見 第 34 條)外,生成器還有一個高級功能,就是可以把調用者通過 throw 方法傳過來的 Exception 實例重新拋出。這個 throw 方法用起來很簡單:如果調用這個方法,那么生成器下次推進時,就不會像平常那樣,直接走到下一條 yield 表達式那里,而是通過 throw 方法傳入的異常重新拋出。下面用代碼演示這種效果。
class MyError(Exception):passdef my_generator():yield 1yield 2yield 3it = my_generator()
print(next(it))
print(next(it))
print(it.throw(MyError('test error')))>>>
__main__.MyError: test error
1
2
? 生成器函數可以用標準的 try/expect 復合語句把 yield 表達式包裹起來,如果函數執行到了這條表達式這里,而這次即將繼續執行時,又發現外界通過 throw 方法給自己注入了異常,那么這個異常就會被 try 結構捕獲下來,如果捕獲之后不繼續拋出異常,那么生成器函數就會推進到下一條 yield 表達式(更多異常處理,參見 第 65 條)。
class MyError(Exception):passdef my_generator():yield 1try:yield 2except MyError:print("Get Error")else:yield 3yield 4it = my_generator()
print(next(it)) # Yield 1
print(next(it)) # Yield 2
print(it.throw(MyError('test error')))>>>
1
2
Get Error
4
? 這項機制會在生成器與調用者形成雙向通道(另一種雙向通信通道, 參見第34條),這在某些情況下是有用的。例如,要編寫一個可以重置的計時器程序。筆者定義下面的 Reset 異常與 Timer 生成器方法,讓調用者可以在 timer 給出的迭代器上通過 throw 方法注入 Reset 異常,令計時器重置。
def Reset(exception):passdef timer(period):current = periodwhile current:current -= 1try:yield currentexcept Reset:current = period
? 按照這種寫法,如果 timer 正準備從 yield 表達式往下遞推時,發現有人注入了 Reset 異常,那么它就會把這個異常捕獲下來,并進入 except 分支,在這里它會把表示倒計時的 current 變量調整成最初的 period 值。
? 這個計時器可以與外界某個按秒查詢的輸入機制對接起來。筆者定義一個函數以驅動 timer 生成器所給出的那個 it 迭代器,并根據外界的情況做處理,如果外界要求重置,那就通過 it 迭代器的 throw 方法給計時器注入 Reset 變量,如果外界沒有這樣要求,那就調用 annouce 函數打印所給的倒計時值。
def check_for_reset():# Poll for external event...def announce(remaining):print(f'{remaining} ticks remaining')def run():it = timer(4)while True:try:if check_for_reset():current = it.throw(Reset())else:current = next(it)except StopIteration:breakelse:announce(current)run()>>>
3 ticks remaining
2 ticks remaining
1 ticks remaining
3 ticks remaining
2 ticks remaining
1 ticks remaining
3 ticks remaining
2 ticks remaining
1 ticks remaining
0 ticks remaining
? 這樣寫用了很多嵌套結構,我們要判斷 check_for_reset() 函數的返回值,以確定是應該通過 it.throw 注入 Reset 異常,還是應該通過 next 推進迭代器。如果要是推進迭代器,那么還得捕獲 StopIteration 異常:若是捕獲到了這種異常,那說明迭代器已經走到終點,則要執行 break 跳出 while 循環; 如果沒有捕獲到,則應該采用 annouce 函數打印倒計時。這會讓代碼很亂。
? 有個簡單的辦法,能夠改寫這段代碼,那就是利用可迭代的容器對象(參見 第 31 條)定義一個有狀態的必包(參見 第 38 條)。下面的代碼就寫了這樣一個 Timer 類,并通過它重新實現剛才的 timer 生成器。
class Timer:def __init__(self, period):self.current = periodself.period = perioddef reset(self):self.current = self.perioddef __iter__(self):while self.current:self.current -= 1yield self.current
? 現在,run 函數就好寫多了,因為它只需要用 for 循環 迭代這個 timer 即可。
def run():timer = Timer(4)for current in timer:if check_for_reset():timer.reset()announce(current)
run()>>>
3 ticks remaining
2 ticks remaining
1 ticks remaining
3 ticks remaining
2 ticks remaining
1 ticks remaining
3 ticks remaining
2 ticks remaining
1 ticks remaining
0 ticks remaining
? 這樣寫所輸出的結果與前面一樣,但是這種實現方法理解起來更容易。凡是想用生成器與異常來實現的功能,通常都可以改用異步機制去做(參見 第 60 條)。如果確實遇到了這里講到的這種需求,那么更應該通過可迭代的類實現生成器,而不要用 throw 方法注入異常。