(点击
上方蓝字
,快速关注我们)
来源:goodspeed
segmentfault.com/a/1190000009769387
如有好文章投稿,请点击 → 这里了解详情
最近找到一本python好书《流畅的python》,是到现在为止看到的对python高级特性讲述最详细的一本。看了协程一章,做个读书笔记,加深印象。
协程定义
协程的底层架构是在pep342 中定义,并在python2.5 实现的。
python2.5 中,yield关键字可以在表达式中使用,而且生成器API中增加了 .send(value)方法。生成器可以使用.send(…)方法发送数据,发送的数据会成为生成器函数中yield表达式的值。
协程是指一个过程,这个过程与调用方协作,产出有调用方提供的值。因此,生成器可以作为协程使用。
除了 .send(…)方法,pep342 和添加了 .throw(…)(让调用方抛出异常,在生成器中处理)和.close()(终止生成器)方法。
python3.3后,pep380对生成器函数做了两处改动:
协程生成器的基本行为
首先说明一下,协程有四个状态,可以使用inspect.getgeneratorstate(…)函数确定:
#! -*- coding: utf-8 -*-
import
inspect
# 协程使用生成器函数定义:定义体中有yield关键字。
def
simple_coroutine
()
:
print
(
'-> coroutine started'
)
# yield 在表达式中使用;如果协程只需要从客户那里接收数据,yield关键字右边不需要加表达式(yield默认返回None)
x
=
yield
print
(
'-> coroutine received:'
,
x
)
my_coro
=
simple_coroutine
()
my_coro
# 和创建生成器的方式一样,调用函数得到生成器对象。
# 协程处于 GEN_CREATED (等待开始状态)
print
(
inspect
.
getgeneratorstate
(
my_coro
))
my_coro
.
send
(
None
)
# 首先要调用next()函数,因为生成器还没有启动,没有在yield语句处暂停,所以开始无法发送数据
# 发送 None 可以达到相同的效果 my_coro.send(None)
next
(
my_coro
)
# 此时协程处于 GEN_SUSPENDED (在yield表达式处暂停)
print
(
inspect
.
getgeneratorstate
(
my_coro
))
# 调用这个方法后,协程定义体中的yield表达式会计算出42;现在协程会恢复,一直运行到下一个yield表达式,或者终止。
my_coro
.
send
(
42
)
print
(
inspect
.
getgeneratorstate
(
my_coro
))
运行上述代码,输出结果如下
GEN_CREATED
->
coroutine started
GEN_SUSPENDED
->
coroutine
received
:
42
# 这里,控制权流动到协程定义体的尾部,导致生成器像往常一样抛出StopIteration异常
Traceback
(
most recent call
last
)
:
File
"/Users/gs/coroutine.py"
,
line
18
,
in
<
module
>
my_coro
.
send
(
42
)
StopIteration
send方法的参数会成为暂停yield表达式的值,所以,仅当协程处于暂停状态是才能调用send方法。如果协程还未激活(GEN_CREATED 状态)要调用next(my_coro) 激活协程,也可以调用my_coro.send(None)
如果创建协程对象后立即把None之外的值发给它,会出现下述错误:
>>>
my_coro
=
simple_coroutine
()
>>>
my_coro
.
send
(
123
)
Traceback
(
most recent call
last
)
:
File
"/Users/gs/coroutine.py"
,
line
14
,
in
<
module
>
my_coro
.
send
(
123
)
TypeError
:
can
'
t
send
non
-
None
value
to
a
just
-
started
generator
仔细看错误消息
can’t send non-None value to a just-started generator
最先调用next(my_coro) 这一步通常称为”预激“(prime)协程—即,让协程向前执行到第一个yield表达式,准备好作为活跃的协程使用。
再看一个两个值得协程
def
simple_coro2
(
a
)
:
print
(
'-> coroutine started: a ='
,
a
)
b
=
yield
a
print
(
'-> Received: b ='
,
b
)
c
=
yield
a
+
b
print
(
'-> Received: c ='
,
c
)
my_coro2
=
simple_coro2
(
14
)
print
(
inspect
.
getgeneratorstate
(
my_coro2
))
# 这里inspect.getgeneratorstate(my_coro2) 得到结果为 GEN_CREATED (协程未启动)
next
(
my_coro2
)
# 向前执行到第一个yield 处 打印 “-> coroutine started: a = 14”
# 并且产生值 14 (yield a 执行 等待为b赋值)
print
(
inspect
.
getgeneratorstate
(
my_coro2
))
# 这里inspect.getgeneratorstate(my_coro2) 得到结果为 GEN_SUSPENDED (协程处于暂停状态)
my_coro2
.
send
(
28
)
# 向前执行到第二个yield 处 打印 “-> Received: b = 28”
# 并且产生值 a + b = 42(yield a + b 执行 得到结果42 等待为c赋值)
print
(
inspect
.
getgeneratorstate
(
my_coro2
))
# 这里inspect.getgeneratorstate(my_coro2) 得到结果为 GEN_SUSPENDED (协程处于暂停状态)
my_coro2
.
send
(
99
)
# 把数字99发送给暂停协程,计算yield 表达式,得到99,然后把那个数赋值给c 打印 “-> Received: c = 99”
# 协程终止,抛出StopIteration
运行上述代码,输出结果如下
GEN_CREATED
->
coroutine
started
:
a
=
14
GEN_SUSPENDED
->
Received
:
b
=
28
->
Received
:
c
=
99
Traceback
(
most recent call
last
)
:
File
"/Users/gs/coroutine.py"
,
line
37
,
in
<
module
>
my_coro2
.
send
(
99
)
StopIteration
simple_coro2 协程的执行过程分为3个阶段,如下图所示
-
调用next(my_coro2),打印第一个消息,然后执行yield a,产出数字14.
-
调用my_coro2.send(28),把28赋值给b,打印第二个消息,然后执行 yield a + b 产生数字42
-
调用my_coro2.send(99),把99赋值给c,然后打印第三个消息,协程终止。
使用装饰器预激协程
我们已经知道,协程如果不预激,不能使用send() 传入非None 数据。所以,调用my_coro.send(x)之前,一定要调用next(my_coro)。为了简化,我们会使用装饰器预激协程。
from
functools
import
wraps
def
coroutinue
(
func
)
:
'''
装饰器: 向前执行到第一个`yield`表达式,预激`func`
:param func: func name
:return: primer
'''
@
wraps
(
func
)
def
primer
(
*
args
,
**
kwargs
)
:
# 把装饰器生成器函数替换成这里的primer函数;调用primer函数时,返回预激后的生成器。
gen
=
func
(
*
args
,
**
kwargs
)
# 调用被被装饰函数,获取生成器对象
next
(
gen
)
# 预激生成器
return
gen
# 返回生成器
return
primer
# 使用方法如下
@
coroutinue
def
simple_coro
(
a
)
:
a
=
yield
simple_coro
(
12
)
# 已经预激
终止协程和异常处理
协程中,为处理的异常会向上冒泡,传递给next函数或send方法的调用方,未处理的异常会导致协程终止。
看下边这个例子
#! -*- coding: utf-8 -*-
from
functools
import
wraps
def
coroutinue
(
func
)
:
'''
装饰器: 向前执行到第一个`yield`表达式,预激`func`
:param func: func name
:return: primer
'''
@
wraps
(
func
)
def
primer
(
*
args
,
**
kwargs
)
:
# 把装饰器生成器函数替换成这里的primer函数;调用primer函数时,返回预激后的生成器。
gen
=
func
(
*
args
,
**
kwargs
)
# 调用被被装饰函数,获取生成器对象
next
(
gen
)
# 预激生成器
return
gen
# 返回生成器
return
primer
@
coroutinue
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
(
coro_avg
.
send
(
40
))
print
(
coro_avg
.
send
(
50
))
print
(
coro_avg
.
send
(
'123'
))
# 由于发送的不是数字,导致内部有异常抛出。
执行上述代码结果如下
40.0
45.0
Traceback
(
most recent call
last
)
:
File
"/Users/gs/coro_exception.py"
,
line
37
,
in
<
module
>
print
(
coro_avg
.
send
(
'123'
))
File
"/Users/gs/coro_exception.py"
,
line
30
,
in
averager
total
+=
term
TypeError
:
unsupported operand type
(
s
)
for
+=:
'float'
and
'str'
出错的原因是发送给协程的’123’值不能加到total变量上。出错后,如果再次调用 coro_avg.send(x) 方法 会抛出 StopIteration 异常。
由上边的例子我们可以知道,如果想让协程退出,可以发送给它一个特定的值。比如None和Ellipsis。(推荐使用Ellipsis,因为我们不太使用这个值)
从Python2.5 开始,我们可以在生成器上调用两个方法,显式的把异常发给协程。
这两个方法是throw和close。
generator.throw(exc_type[, exc_value[, traceback]])
这个方法使生成器在暂停的yield表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式,而产出的值会成为调用throw方法得到的返回值。如果没有处理,则向上冒泡,直接抛出。
generator.close()
生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器没有处理这个异常或者抛出了StopIteration异常,调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。
示例: 使用close和throw方法控制协程。
import
inspect
class
DemoException
(
Exception
)
:
pass
@
coroutinue
def
exc_handling
()
:
print
(
'-> coroutine started'
)
while
True
:
try
:
x
=
yield
except
DemoException