第 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 方法注入异常。