协程,从yield说起

Ref: 《Fluent Python》 第16章 协程

协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。

def simple_coroutine(a): # 协程用生成器函数定义,里面有yield
print(f'-> Started: a = {a}')
b = yield a # yield左边是接收值,右边是产出值
print('-> Received:', b)

my_coro = simple_coroutine(14)
my_coro # 调用得到协程
<generator object simple_coroutine at 0x1069e5230>
next(my_coro) # 预激活协程,让协程向前执行到第一个yield
-> Started: a = 14
my_coro.send(42) # 控制权流动到协程末尾,抛出StopIteration
-> Received: 42
---------------------------------------------------------------------------

StopIteration                             Traceback (most recent call last)

Cell In[3], line 1
----> 1 my_coro.send(42) # 控制权流动到协程末尾,抛出StopIteration

可以通过inspect.getgeneratorstate()确定协程的状态:

  • GEN_CREATED
  • GEN_RUNNING
  • GEN_SUSPENDED
  • GEN_CLOSED
from inspect import getgeneratorstate
getgeneratorstate(my_coro)
'GEN_CLOSED'

使用协程计算移动平均值

def averager():
'''
向协程不断传入值,计算结束,协程再传出
'''
total = 0
count = 0
average = 0
while True:
term = yield average
total += term
count += 1
average = total / count
avg = averager()
next(avg) # 预激活
avg.send(10)
10.0
avg.send(11)
10.5

下面解决两个问题:

  1. 用装饰器解决每次都要先预激活协程的问题(略)
  2. 如何结束协程的问题

第一个问题的代码忽略,预激协程装饰器

终止协程和异常处理

客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。这两个方法是 throw 和 close。

generator.throw(exc_type[, exc_value[, traceback]])

致使生成器在暂停的 yield 表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个 yield 表达式,而产出的值会成为调用 generator.throw 方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。

generator.close()

致使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常。如果生成器没有处理这个异常,或者抛出了 StopIteration 异常(通常是指运行到结尾),调用方不会报错。如果收到 GeneratorExit 异常,生成器一定不能产出值,否则解释器会抛出RuntimeError 异常。生成器抛出的其他异常会向上冒泡,传给调用方。

class DemoException(Exception):
...

def demo_exc_handling():
print('started')
while True:
try:
x = yield
except DemoException:
print('DemoException Handled, Continuing...')
else:
print(f'received: {x}')

exe_coro = demo_exc_handling()
next(exe_coro)
exe_coro.send(11)
exe_coro.send(11)
# 传入DemoException不会导致协程终止
exe_coro.throw(DemoException())
# 传入其他未处理的异常,协程会终止
exe_coro.close()
started
received: 11
received: 11
DemoException Handled, Continuing...

使用yield from

首先yield from 可以简化for循环中的yield表达式,

yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。

def gen():
for c in 'AB':
yield c
for i in range(3):
yield i

list(gen())
['A', 'B', 0, 1, 2]
def gen():
'''use yield from'''
yield from 'AB'
yield from range(3)

list(gen())
['A', 'B', 0, 1, 2]

但yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。用脚本例子说明:

'''
脚本从一个字典中读取虚构的七年级男女学生的体重和身高。例如,'boys;m' 键对应于 9 个男学生的身高(单位是米), 'girls;kg' 键对应于 10 个女学生的体重(单位是千克)。这个脚本把各组数据传给前面定义的 averager 协程,然后生成一个报告
'''
from collections import namedtuple
Result = namedtuple('Result', 'count average')
# 子生成器
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total/count
return Result(count, average) # 返回的result会成为grouper函数中yield from表达式的值

# 委派生成器
def grouper(results, key):
while True: # 每次循环新建一个average实例
'''
grouper 发 送 的 每 个 值 都 会 经 由 yield from 处 理, 通 过 管 道 传 给 averager 实 例。
grouper 会在 yield from 表达式处暂停,等待 averager 实例处理客户端发来的值。
averager 实例运行完毕后,返回的值绑定到 results[key] 上。 while 循环会不断创建
averager 实例,处理更多的值。
'''
results[key] = yield from averager() #

# 客户端代码,即调用方
def main(data):
results = {}
for key, values in data.items():
group = grouper(results, key)
next(group)
for value in values:
group.send(value) # 把各个 value 传给 grouper。传入的值最终到达 averager 函数中 term = yield 那一行;grouper 永远不知道传入的值是什么。
group.send(None) # 重要! 把 None 传入 grouper,导致当前的 averager 实例终止,也让 grouper 继续运行,再创建一个 averager 实例,处理下一组值
# print(results) # 如果要调试,去掉注释
report(results)

# 输出报告
def report(results):
for key, result in sorted(results.items()):
group, unit = key.split(';')
print('{:2} {:5} averaging {:.2f}{}'.format(
result.count, group, result.average, unit))
data = {
'girls;kg':
[40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
'girls;m':
[1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
'boys;kg':
[39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
'boys;m':
[1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}

main(data)
 9 boys  averaging 40.42kg
 9 boys  averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m

解释一下上面代码发生了什么:
image.png

  • 外层for循环每次迭代会新建一个grouper实例,赋值给group变量; group是委派生成器。
  • 调用 next(group),预激委派生成器 grouper,此时进入 while True 循环,调用子生成器 averager 后,在 yield from 表达式处暂停。
  • 内层 for 循环调用 group.send(value),直接把值传给子生成器 averager。同时,当前的 grouper 实例(group)在 yield from 表达式处暂停。
  • 内层循环结束后, group 实例依旧在 yield from 表达式处暂停,因此, grouper 函数定义体中为 results[key] 赋值的语句还没有执行。
  • 如果外层 for 循环的末尾没有 group.send(None),那么 averager 子生成器永远不会终止,委派生成器 group 永远不会再次激活,因此永远不会为 results[key] 赋值。
  • 外层 for 循环重新迭代时会新建一个 grouper 实例,然后绑定到 group 变量上。前一个grouper 实例(以及它创建的尚未终止的 averager 子生成器实例)被垃圾回收程序回收。

yield from 意义

  • 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
  • 使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的 next() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
  • 生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr)异常抛出。
  • yield from 表达式的值是子生成器终止时传给 StopIteration 异常的第一个参数

对于RESULT = yield from EXPR的伪代码:(这块看看就行了)

_i = iter(EXPR) # EXPR是任何可迭代的对象
try:
_y = next(_i) # 预激子生成器,结果保存在_y作为产出的第一个值
except StopIteration as _e:
_r = _e.value #如果抛出 StopIteration 异常,获取异常对象的 value 属性,赋值给 _r
else:
while 1: # 运行这个循环时,委派生成器会阻塞,只作为调用方和子生成器之间的通道。
try:
_s = yield _y # 产出子生成器当前产出的元素;等待调用方发送 _s 中保存的值。
except GeneratorExit as _e: # 这一部分用于关闭委派生成器和子生成器。因为子生成器可以是任何可迭代的对象,所以可能没有 close 方法。
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e: # 这一部分处理调用方通过 .throw(...) 方法传入的异常。
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else: # 如果子生成器有 throw 方法,调用它并传入调用方发来的异常。
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else: # 如果产出值时没有异常
try: # 尝试让子生成器向前执行
if _s is None: # 如果调用方最后发送的值是 None,在子生成器上调用 next 函数,否则调用 send 方法
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e: # 如果子生成器抛出 StopIteration 异常,获取 value 属性的值,赋值给 _r,然后退出循环,让委派生成器恢复运行
_r = _e.value
break
RESULT = _r # 返回的结果(RESULT)是 _r,即整个 yield from 表达式的值。
文章作者: Met Guo
文章链接: https://guoyujian.github.io/2024/03/14/%E5%8D%8F%E7%A8%8B%EF%BC%8C%E4%BB%8Eyield%E8%AF%B4%E8%B5%B7/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Gmet's Blog