理解 Python 的 yield from
想象这样一个场景,你有一个协程,其中包含了许多的 yield:
>>> def somecoro():
... do_something_necessary
... while True:
... recv1 = yield
... do something with recv1
... recv2 = yield
... do something with recv2
... recv3 = yield
... do something with recv3
协程依赖于外界发送的值,依次执行。现在,你想要将这多个 yield 部分重构成一个新的协程,假如说是 refactor_yield,这样就可以减少冗余代码:
>>> def refactor_yield():
... while True:
... recv = yield
... do_something depend on recv
但是,你不能单独使用这个协程,因为你依赖函数 somecoro() 完成必要的设置。所以,现在代码变成这样了:
>>> def somecoro1():
... do_something
... subcoro = refactor_yield()
... next(subcoro) # 准备好子协程
... while True:
... recv = yield # 接受从外界发送的值
... subcoro.send(recv) # 将值发送给子协程处理
从上面我们可以看到,yield 的缺点是只允许协程与外界直接交互而不能使得子协程与外界直接交互。而且,我们这里还没有考虑子协程抛出异常,返回等情况。
为了解决这个问题,Python 引入了 yield from,其 基本语法如下:
yield from <expr>
与 yield 类似,yield from 也是一个表达式,并且任何包含了 yield from 的函数都将变成生成器。<expr> 要求必须是可迭代的,即 iter(expr) 将返回一个迭代器,一个简单的例子如下:
>>> def test():
... yield from donotknow()
...
>>> t=test()
>>> type(t)
generator
yield from 允许一个生成器将它的部分工作委托给另一个生成器(子生成器,subgenerator)执行,在这里,我们将包含 yield from 表达式的生成器(例如,test() 函数)称为委托生成器,记为 a;将 yield from 后面的称为子生成器/子迭代器(例如,donotknow() 函数),记为 b。那么 yield from 的作用可以用下面的生成器协议总结:
- 任何从子迭代器 b yield 回的值都将直接传递给委托生成器 a 的调用者,从调用者角度来看,相当于委托迭代器 a 直接 yield 回值。即
yield from iterable等价于for i in iterable: yield i - 调用委托生成器 a 的 send() 发送的任何值都将被直接发送给子迭代器 b。如果发送的值为 None,等价于调用子迭代器 b 的 __next__() 方法;如果不为 None,那么就调用子迭代器 b 的 send() 方法发送。(注意,当调用了子迭代器 b 的 send() 或者 __next__() 方法,那么此时委托生成器 a 就会暂停执行,转而去执行子迭代器 b,下面的 throw 方法类似理解)。如果此次调用产生了
StopIteration(即子迭代器停止了执行),那么委托生成器 a 将会被恢复执行。 - 调用委托生成器 a 的 throw() 方法发送任何除了
GeneratorExit的异常都将被直接发送给子迭代器 b。同理,如果这次调用产生了StopIteration异常,那么委托生成器 a 将被恢复执行 - 如果向委托生成器发送了
GeneratorExit异常或者调用委托生成器的 close() 方法,那么首先子迭代器 b 的 close() 方法就会被调用(如果有的话)。随后,委托生成器就会引发GeneratorExit异常,从而退出 - 如果迭代器 b 中有
return value,那么当 b 执行 return 返回时,语义上等价于raise StopIteration(value)(迭代器总是通过引发StopIteration异常停止的),之后该 value 将成为 yield from 表达式的值。 - 在任意运行时刻,如果迭代器 b 抛出了未捕获的异常,那么该异常将会被传播给委托生成器 a
下面是个简单例子:
>>> def accumulate():
... tally = 0
... while 1:
... next = yield
... print('recv: {}'.format(next))
... if next is None:
... return tally
... tally += next
...
>>> def gather_tallies(tallies):
... while 1:
... tally = yield from accumulate()
... print('tally is: {}'.format(tally))
... tallies.append(tally)
...
>>> tallies = []
>>> acc = gather_tallies(tallies)
>>> next(acc) # 准备好协程
>>> for i in range(4):
... acc.send(i)
...
recv: 0
recv: 1
recv: 2
recv: 3
>>> acc.send(None) # 停止子协程
recv: None
tally is: 6
>>> for i in range(5):
... acc.send(i)
...
recv: 0
recv: 1
recv:
