第16章–协程
协程是用户态的轻量级线程,一个线程内可以有多个协程,协程之间的切换由用户决定,不涉及线程切换跟进程切换,所以切换效率最高
进程线程的切换是系统级的,是抢占式切换,协程的切换是用户态的,是非抢占式切换,协程主动交出控制权,协程概念相关的知识可以参考廖雪峰的网站–https://www.liaoxuefeng.com/wiki/897692888725344/923057403198272
本章的内容
• 生成器作为协程使用时的行为和状态
• 使用装饰器自动预激协程
• 调用方如何使用生成器对象的 .close() 和 .throw(…) 方法控制协程
• 协程终止时如何返回值
• yield from 新句法的用途和语义
• 使用案例——使用协程管理仿真系统中的并发活动
协程的状态
协程可以是下面四个状态之一,可以用inspect.getgeneratorstate()函数来获取协程状态
- ‘GEN_CREATED’ 等待开始执行。
- 'GEN_RUNNING’解释器正在执行。
- ‘GEN_SUSPENDED’ 在 yield 表达式处暂停。
- ‘GEN_CLOSED’ 执行结束。
from inspect import getgeneratorstate
def simple_coro2(a):
print("-> Started my_coro: a = ",a)
b=yield a
print("-> Received: b=",b)
c=yield a+b
print("-> Received: c=",c)
my_coro=simple_coro2(14)
print(getgeneratorstate(my_coro)) # GEN_CREATED
print(next(my_coro)) # -> Started my_coro: a = 14 14
print(getgeneratorstate(my_coro)) # GEN_SUSPENDED
my_coro.send(28) # -> Received: b= 28
try:
my_coro.send(99) # -> Received: c= 99
except StopIteration as e:
print("StopIteration is catched ") # StopIteration is catched
print(getgeneratorstate(my_coro)) # GEN_CLOSED
文中对上述代码的流程做了一个分割
因为 send 方法的参数会成为暂停的 yield 表达式的值,所以,仅当协程处于暂停状态时才
能调用 send 方法,例如 my_coro.send(42)。不过,如果协程还没激活(即,状态是 ‘GEN_
CREATED’),情况就不同了。因此,始终要调用 next(my_coro) 激活协程——也可以调用
my_coro.send(None),效果一样。
my_coro.send之前没有调用next(my_coro),导致协程处于GEN_CRETED状态,此时用my_coro.send()会报错
from inspect import getgeneratorstate
def simple_coro2(a):
print("-> Started my_coro: a = ",a)
b=yield a
print("-> Received: b=",b)
c=yield a+b
print("-> Received: c=",c)
my_coro=simple_coro2(14)
print(getgeneratorstate(my_coro)) # GEN_CREATED
my_coro.send(28) # TypeError: can't send non-None value to a just-started generator
预激协程的装饰器
自己实现一个预激协程的装饰器,装饰器中自动调用一次next,让返回的gen已经处于GEN_SUSPENEDD状态
from inspect import getgeneratorstate
from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args,**kwargs):
gen=func(*args,**kwargs)
next(gen) # 装饰器中自动调用一次next,让返回的gen已经处于GEN_SUSPENEDD状态
return gen
return primer
@coroutine
def averager():
total=0.0
count=0
average=None
while True:
term=yield average
total+=term
count+=1
average=total/count
coro_avg=averager()
print(getgeneratorstate(coro_avg)) # GEN_SUSPENDED
#此时可以直接调用send,因为装饰器已经调用了一次next
print(coro_avg.send(10)) # 10.0
print(coro_avg.send(20)) # 15.0
终止协程和异常处理
- generator.throw(exc_type[, exc_value[, traceback]])
致使生成器在暂停的 yield 表达式处抛出指定的异常。如果生成器处理了抛出的异常,
代码会向前执行到下一个 yield 表达式,而产出的值会成为调用 generator.throw 方法
得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下
文中。 - generator.close()
致使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常。如果生成器没有处
理这个异常,或者抛出了 StopIteration 异常(通常是指运行到结尾),调用方不会
报错。如果收到 GeneratorExit 异常,生成器一定不能产出值,否则解释器会抛出
RuntimeError 异常。生成器抛出的其他异常会向上冒泡,传给调用方。
"""
Coroutine closing demonstration::
# BEGIN DEMO_CORO_EXC_1
>>> exc_coro = demo_exc_handling()
>>> next(exc_coro)
-> coroutine started
>>> exc_coro.send(11)
-> coroutine received: 11
>>> exc_coro.send(22)
-> coroutine received: 22
>>> exc_coro.close()
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(exc_coro)
'GEN_CLOSED'
# END DEMO_CORO_EXC_1
Coroutine handling exception::
# BEGIN DEMO_CORO_EXC_2
>>> exc_coro = demo_exc_handling()
>>> next(exc_coro)
-> coroutine started
>>> exc_coro.send(11)
-> coroutine received: 11
>>> exc_coro.throw(DemoException)
*** DemoException handled. Continuing...
>>> getgeneratorstate(exc_coro)
'GEN_SUSPENDED'
# END DEMO_CORO_EXC_2
Coroutine not handling exception::
# BEGIN DEMO_CORO_EXC_3
>>> exc_coro = demo_exc_handling()
>>> next(exc_coro)
-> coroutine started
>>> exc_coro.send(11)
-> coroutine received: 11
>>> exc_coro.throw(ZeroDivisionError)
Traceback (most recent call last):
...
ZeroDivisionError
>>> getgeneratorstate(exc_coro)
'GEN_CLOSED'
# END DEMO_CORO_EXC_3
"""
# BEGIN EX_CORO_EXC
class DemoException(Exception):
"""An exception type for the demonstration."""
def demo_exc_handling():
print('-> coroutine started')
while True:
try:
x = yield
except DemoException: # <1>
print('*** DemoException handled. Continuing...')
else: # <2>
print('-> coroutine received: {!r}'.format(x))
raise RuntimeError('This line should never run.') # <3>
# END EX_CORO_EXC
获取生成器的return值
要想获取return的值而不是yield的值,在yield不再返回值后就会返回return的值,在调用的地方捕获StopIteration可以拿到return值
def fun():
arr=[1,2,3,4,5,6]
for i in arr:
yield i
return 666
new_gen=fun()
print(new_gen) # <generator object fun at 0x000001D886B10468>
print(next(new_gen)) # 1
print(next(new_gen)) # 2
while True:
try:
print(next(new_gen)) #3 4 5 6
except StopIteration as e:
print(e.value) #666
break
yield from
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,
这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加
大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。
"""
A coroutine to compute a running average.
Testing ``averager`` by itself::
>>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(10)
>>> coro_avg.send(30)
>>> coro_avg.send(6.5)
>>> coro_avg.send(None)
Traceback (most recent call last):
...
StopIteration: Result(count=3, average=15.5)
Driving it with ``yield from``::
>>> def summarize(results):
... while True:
... result = yield from averager()
... results.append(result)
...
>>> results = []
>>> summary = summarize(results)
>>> next(summary)
>>> for height in data['girls;m']:
... summary.send(height)
...
>>> summary.send(None)
>>> for height in data['boys;m']:
... summary.send(height)
...
>>> summary.send(None)
>>> results == [
... Result(count=10, average=1.4279999999999997),
... Result(count=9, average=1.3888888888888888)
... ]
True
"""
# BEGIN YIELD_FROM_AVERAGER
from collections import namedtuple
Result = namedtuple('Result', 'count average')
# the subgenerator
def averager(): # <1>
total = 0.0
count = 0
average = None
while True:
term = yield # <2>
if term is None: # <3>
break
total += term
count += 1
average = total/count
return Result(count, average) # <4>
# the delegating generator
def grouper(results, key): # <5>
while True: # <6>
results[key] = yield from averager() # <7>
# results[key] = yield from averager()
# yield
# the client code, a.k.a. the caller
def main(data): # <8>
results = {}
for key, values in data.items():
group = grouper(results, key) # <9>
next(group) # <10>
for value in values:
group.send(value) # <11>
# print(results)
group.send(None) # important! <12>
# print(results)
print(results) # uncomment to debug
report(results)
# output report
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],
}
if __name__ == '__main__':
main(data)
# END YIELD_FROM_AVERAGER
代码注释
1 与示例 16-13 中的 averager 协程一样。这里作为子生成器使用。
2 main 函数中的客户代码发送的各个值绑定到这里的 term 变量上。
3 至关重要的终止条件。如果不这么做,使用 yield from 调用这个协程的生成器会永远
阻塞。
4 返回的 Result 会成为 grouper 函数中 yield from 表达式的值。
5 grouper 是委派生成器。
6 这个循环每次迭代时会新建一个 averager 实例;每个实例都是作为协程使用的生成器
对象。
7 grouper 发送的每个值都会经由 yield from 处 理, 通 过 管 道 传 给 averager 实 例。
grouper 会在 yield from 表达式处暂停,等待 averager 实例处理客户端发来的值。
averager 实例运行完毕后,返回的值绑定到 results[key] 上。while 循环会不断创建
averager 实例,处理更多的值。
8 main 函数是客户端代码,用 PEP 380 定义的术语来说,是“调用方”。这是驱动一切的
函数。
9 group 是调用 grouper 函数得到的生成器对象,传给 grouper 函数的第一个参数是
results,用于收集结果;第二个参数是某个键。group 作为协程使用。
10 预激 group 协程。
11 把各个 value 传给 grouper。传入的值最终到达 averager 函数中 term = yield 那一行;
grouper 永远不知道传入的值是什么。
12 把 None 传入 grouper,导致当前的 averager 实例终止,也让 grouper 继续运行,再创
建一个 averager 实例,处理下一组值。
其他说明
• 外层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 子生成器实例)被垃圾回收程序回收。
资料参考
代码中grouper函数为什么要用while True的循环的解释,虽然看了也还是有点懵,希望有大佬可以看完跟我解释一下
https://stackoverflow.com/questions/53687825/unsure-of-why-the-stopiteration-isnt-handled-by-yield-from
官方对yield from的介绍
https://www.python.org/dev/peps/pep-0380/
其他博文
https://wenkefendou.gitbooks.io/python3-learning/content/yield_from.html
体会
yield from 是一个魔法方法,还没有接触过具体的使用场景,理解有点困难,先简单了解一下