(2022.06.13 Mon)
协程往往和线程做对比。协程也是并发的一种,协程与线程不同之处在于:
- 线程的调度由CPU执行,协程的调用由开发者写的函数执行
- 协程更轻量级
- 协程运行与同一个线程中
在Python中协程可由生成器实现,某种程度上可以作为流程控制工具的yield
也可以实现协作式多任务,似乎是为协程而设计。
生成器的基本行为
首先定义一个生成器函数。
def simple_coroutine(): # 1
print('-> coroutine started')
x = yield # 2
print('-> coroutine received:', x)
对simple_coroutine
实例,得到一个生成器对象
>> a = simple_coroutine()
>> a
<generator object simple_coroutine at 0x7ffe3131cdd0>
首先回忆生成器的使用方法。yield
关键字右边的部分如果为空,则该yield
关键字只接受用户传入的值,不返回值给用户,或返回None
;yield
右边的变量是每次通过next
方法调用生成器时返回给用户的变量。yield
左边赋值的变量,即x = yield
,表示用户在通过send
方法调用生成器时传入的值将会保存在变量x
中。
通过next
函数调用生成器,并用send
方法传入数值到生成器中。
>> next(a)
-> coroutine started
>> a.send(998)
-> coroutine received: 998
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
(2022.06.14 Tues)
上面的例子yield
的等号左边被赋值,也就是caller可以传递值进生成器,另有一种不赋值,不能传值进生成器的情况。比如用yield
产生num
到100间所有的奇数的方式。
def generator_odd(num):
if num > 100:
num = num % 100
for i in range(num, 101):
if i%2 == 1:
yield i
调用
>> geno = generator_odd(83)
>> next(geno)
83
>> type(geno)
<class 'generator'>
>> list(geno)
[85, 87, 89, 91, 93, 95, 97, 99]
协程的状态
协程分为四种状态,通过inspect.getgeneratorstate
可查询生成器处在协程的哪种状态。四种状态分别是
- GEN_CREATED:生成器等待开始执行,创建之后
- GEN_RUNNING:生成器解释执行中
- GEN_SUSPENDED:在表达式处暂停
- GEN_CLOSE:执行结束
>> from inspect import getgeneratorstate
>> getgeneratorstate(a)
'GEN_CLOSED'
协程的预激(prime)
在生成器的第一次调用时,如果使用send
方法传入一个非None
到生成器中,则会返回TypeError
错误。
>> a = simple_coroutine()
>> a.send(10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started coroutine
通常,在一个生成器第一步使用时,调用next
方法,用以预激协程,也就是让协程向前执行到第一个yield
表达式,准备好作为活跃的协程使用。
预激的方法有两种:
- 调用
next
方法 - 调用
send
方法传入None
值
为简化预激协程的过程,可使用装饰器对生成器函数进行装饰。
>> def coroutine(func):
@wraps(func)
def primer(*args, **kwargs):
gen = func(*args, **kwargs) # 调用被装饰的函数,获取生成器对象
next(gen) # 预激生成器
return gen # 返回生成器
return primer
用该协程函数,装饰计算平均值的生成器。
@coroutine
def average():
total, cntr, average = 0, 0, None
while True:
tmp = yield average
total += tmp
cntr += 1
average = total/cntr
调用average
函数创建一个生成器对象,注意因为该函数已经被coroutine
装饰,已经完成了预激,在生成器对象创建之后可直接向其中传入值,而不必预激
>> b = average()
>> type(b)
<class 'generator'>
>> inspect.getgeneratorstate(b)
'GEN_SUSPENDED'
>> b.send(1000)
1000.0
注意到,在创建生成器对象后,查看生成器的状态,因为装饰器中已经完成了预激,所以状态为GEN_SUSPENDED
。作为对比,查看一个为被装饰的生成器返回的状态是'GEN_CREATED'。
def noprime():
c = 1
tmp = yield c
c += 1
>> np = noprime()
>> inspect.getgeneratorstate(np)
'GEN_CREATED'
协程的执行顺序
有这样一个生成器函数
def simple_coro2(a):
print('-> Started: a =', a)
b = yield a
print('-> Received: b =', b)
c = yield a + b
print('-> Received: c =', c)
运行结果如下
>> d = simple_coro2(5)
>> next(d) # 1
-> Started: a = 5 # 2
5 # 3
>> d.send(99)
-> Received: b = 99 # 4
104 # 5
>> d.send(25)
-> Received: c = 25 # 6
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
# 1:预激,运行代码到第一个yield
处,于是有了# 2和# 3
# 2:打印第一个yield
之前的print
指令
# 3:返回yield a
中的a
# 4和# 5:从第一个b = yield a
的等号左边开始运行,到下一个c = yield a + b
的等号右边,于是有了# 5的104,因a+b=104
# 6:运行c = yield a + b
的等号左边,接受了来自用户的send
赋值,并运行到生成器结尾,以StopIteration
退出
从上面的过程可以看到,对于能接受用户赋值的yield
表达式,每次运行到yield
表达式的等号右侧,下次调用next
或send
从yield
的表达式左侧开始运行,顺序如图所示
终止协程和异常处理
继续考虑求平均值的生成器
@coroutine
def average():
total, cntr, average = 0, 0, None
while True:
tmp = yield average
total += tmp
cntr += 1
average = total/cntr
创建生成器,并传递不符合要求的类型
>> f = average()
>> getgeneratorstate(f)
'GEN_SUSPENDED'
>> f.send('s')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in average
TypeError: unsupported operand type(s) for +=: 'int' and 'str'
>> f.send(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
这个案例给出了一种终止协程的方式:发送哨符值,比如None
。使用生成器对象的close
方法可以关闭协程。
致使生成器在暂停的yield
表达式处抛出指定的异常。如果生成
器处理了抛出的异常,代码会向前执行到下一个 yield 表达式,而产
出的值会成为调用 generator.throw 方法得到的返回值。如果生成器
没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。
协程的异常处理,可参考下面例子
class DemoException(Exception):
pass
def demo_exc_handling():
print('-> coroutine started')
while True:
try:
x = yield
except DemoException:
print('*** DemoException handled. Continuing...')
else:
print('-> coroutine received: {!r}'.format(x))
创建生成器对象,并用throw
方法抛出异常
>> de = demo_exc_handing()
>> de.send(None)
-> coroutine started
>> de.throw(DemoException) # 1
** DemoException handled. Continuing...
>> de.send(2) # 2
-> coroutine received: 2
# 1:抛出DemoException
异常
# 2:上一步抛出异常后,协程并没有终止
(2022.06.14 Tues)
让协程返回值
协程返回值最简单的方法是当检测到caller通过send
传入的值为特定值,如None
时,跳出生成器的循环条件,并返回指定的值。考虑计算平均值的例子。
def average():
total, count, average = 0, 0, None
while True:
term = yield
if term is None: break
total += term
count += 1
average = total / count
return (count, average)
调用得到
>> f = average()
>> f.send(None)
>> f.send(1)
>> f.send(3)
>> f.send(10)
>> f.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: (3, 4.666666666666667)
注意到生成器函数的返回结果,即return (count, average)
部分,是StopIteration
的属性,所以可以在调用时catch
这个异常,并调用该属性。
>> f = average()
>> f.send(None) # prime coroutine
>> f.send(10)
>> f.send(20)
>> try:
... f.send(None)
... except StopIteration as s:
... tmp = s.value
...
>> tmp
(2, 15.0)
yield from
yield from
语法出现在Python 3.3之后,其后接应的对象是可迭代对象,迭代器,和生成器,用于简化for
循环中的yield
表达式。仅使用yield
指令的情况下有如下表达:
def gen():
for c in 'AB':
yield c
for i in range(1, 3):
yield i
调用
>> list(gen())
['A', 'B', 1, 2]
如果使用yield from
表达,gen
方法可以简化为下面这种形式,且调用结果完全相同。
def gen():
yield from 'AB'
yield from range(1, 3)
yield from
加可迭代对象,可以把可迭代对象中的每个元素一一yield
出来。
yield from
后面接上生成器,就得到了生成器的嵌套。尽管生成器的嵌套并非一定要用yield from
方法,但这个方法可以避免不必要的麻烦。
研究生成器的嵌套前首先明确几个概念:
- 调用方:调⽤委派⽣成器的客户端(调⽤⽅)代码
- 委托生成器:包含
yield from
表达式的⽣成器函数 - 子生成器:
yield from
后⾯加的⽣成器函数
下面用求平均的案例来解释上面几个概念
# 子生成器
def average():
total, cntr, average = 0, 0, None
while True:
tmp = yield average
total += tmp
cntr += 1
average = total/cntr
# 委托生成器
def proxy_gen():
while True:
yield from average()
# 调用方
def main():
calc_average = proxy_gen()
next(calc_average) # 预激生成器
print(calc_average.send(10)) # 10.0
print(calc_average.send(20)) # 15.0
print(calc_average.send(30)) # 20.0
委托生成器的作用是在调用方和子生成器之间建立一个双向通道。
下面查看一下当出现异常时,对委托生成器做修改后如何处理异常。
def average_gen():
total =0
count =0
average =0
while True:
new_num = yield average
if new_num is None:
break
count += 1
total += new_num
average = total/count
return total, count, average
def proxy_gen():
while True:
# 只有⼦⽣成器要结束(return)了,yield from左边的变量才会被赋值,后⾯的代码才会执⾏。
total, count, average = yield from average_gen()
print("计算完毕\n 总共传⼊{} 个数值,总和:{},平均数:{}".format(count, total, average))
def main():
calc_average = proxy_gen()
next(calc_average)
print(calc_average.send(10)) #
print(calc_average.send(20)) #
print(calc_average.send(30)) #
calc_average.send(None)
返回结果
>> main()
10.0
15.0
20.0
计算完毕!!
总共传⼊3 个数值,总和:60,平均数:20.0
如果避开委托生成器,直接对子生成器发送值和None
,则最后返回StopIteration
和对应的total等值。使用了委托生成器则能优雅的处理异常并返回打印结果。
>>> a = average_gen()
>>> a.send(None)
0
>>> a.send(10)
10.0
>>> a.send(20)
15.0
>>> a.send(40)
23.333333333333332
>>> a.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: (70, 3, 23.333333333333332)
当然委托生成器不只是处理异常,StopIteration
异常处理完全可以自己手写完成。委托生成器做了更多。
下面的伪代码,等效于委派生成器中的RESULT = yield from EXPR
语句。以下代码过于复杂,可以简单的理解为yield from
帮忙做了很多的异常处理。
_i = iter(EXPR)
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while 1:
try:
_s = yield _y
except GeneratorExit as _e:
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e:
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else:
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else:
try:
if _s is None:
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e:
_r = _e.value
break
RESULT = _r
以上代码的说明如下:
- 迭代器(即可指⼦⽣成器)产⽣的值直接返还给调⽤者
- 任何使⽤send()⽅法发给委派⽣产器(即外部⽣产器)的值被直接传递给迭代器。如果
send
值是None
,则调⽤迭代器next()
⽅法;如果不为None
,则调⽤迭代器的send()
⽅法。如果对迭代器的调⽤产⽣StopIteration
异常,委派⽣产器恢复继续执⾏yield from
后⾯的语句;若迭代器产⽣其他任何异常,则都传递给委派⽣产器。 - ⼦⽣成器可能只是⼀个迭代器,并不是⼀个作为协程的⽣成器,所以它不⽀持
throw()
和close()
⽅法,即可能会产⽣AttributeError
异常。 - 除了
GeneratorExit
异常外的其他抛给委派⽣产器的异常,将会被传递到迭代器的throw()
⽅法。如果迭代器throw()
调⽤产⽣了StopIteration
异常,委派⽣产器恢复并继续执⾏,其他异常则传递给委派⽣产器。
- 如果
GeneratorExit
异常被抛给委派⽣产器,或者委派⽣产器的close()
⽅法被调⽤,如果迭代器有close()
的话也将被调⽤。如果close()
调⽤产⽣异常,异常将传递给委派⽣产器。否则,委派⽣产器将抛出GeneratorExit
异常。 - 当迭代器结束并抛出异常时,
yield from
表达式的值是其StopIteration
异常中的第⼀个参数。 - ⼀个⽣成器中的
return expr
语句将会从⽣成器退出并抛出StopIteration(expr)
异常。
Reference
1 流畅的Python,Luciano R. 著,安道等译,中国工信出版社,人民邮电出版社
2 百度文库-Python并发编程之深入理解yieldfrom语法(八)
网友评论