理解 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: 2
recv: 3
recv: 4
>>> acc.send(None)
recv: None
tally is: 10
>>> tallies
[6,10]
>>> acc.throw(TypeError)  # TypeError 来自 accumulate 函数
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in gather_tallies
  File "<stdin>", line 4, in accumulate
TypeError

从上面的例子可以看到,使用 send() 方法发送的值将被直接发送子协程 accumulate() 中了,throw() 同理。并且,当子协程返回时,return 后的值成为 yield from 表达式的值。


总结:

yield from 允许外界与子协程直接交互,这样就允许代码重构:把一部分包含 yield 的代码放到另外的函数,再使用 yield from 调用该函数。这样,就实现委托工作了。

参考:

PEP 380 – Syntax for Delegating to a Subgenerator In practice, what are the main uses for the new “yield from” syntax in Python 3.3?

posted @ 2017-07-26 14:43  天涯海角路  阅读(142)  评论(0)    收藏  举报