Python 协程深入理解
从语法上来看,协程和生成器类似,都是定义体中包含yield关键字的函数。 yield在协程中的用法:
- 在协程中yield通常出现在表达式的右边,例如:datum = yield,可以产出值,也可以不产出--如果yield关键字后面没有表达式,那么生成器产出None.
- 协程可能从调用方接受数据,调用方是通过send(datum)的方式把数据提供给协程使用,而不是next(...)函数,通常调用方会把值推送给协程。
协程可以把控制器让给中心调度程序,从而激活其他的协程
所以总体上在协程中把yield看做是控制流程的方式
了解协程的过程
def simple_coroutine():
print('coroutine start')
x = yield
print('coroutine received :', x)
my_coro = simple_coroutine()
next(my_coro)
my_coro.send(10)
运行结果
coroutine start
coroutine received : 10
Traceback (most recent call last):
File "d:\Code\Python\day6\app.py", line 19, in <module>
my_coro.send(10)
StopIteration
yield
右边没有没有表达式,默认产出的值是None
,刚开始调用next()
方法,目的在于激活协程,程序就会运行到x = yield
处,这里需要注意,这里程序运行到x = yield
处,并没有将值赋值给x,而是计算yield 后面的值表达式,然后返回给next()
方法,当这个生成器send()
一个值给协程之后,从暂定处yield
将send
的这个值赋值给x
,然后继续运行,直到运行到下一个yield
处。
当程序运行到最后,会自动抛出一个StopIteration
的异常,当捕获异常之后,可以找到这个生成器最后的值
def simple_coroutine():
print('coroutine start')
x = yield
print('coroutine received :', x)
try:
my_coro = simple_coroutine()
next(my_coro)
my_coro.send(10)
except StopIteration as e:
print('执行完毕之后的值:', e.value)
运行结果
coroutine start
coroutine received : 10
执行完毕之后的值: None
与JavaScript Generator
类似,看一个例子
def simple_coroutine(x, y):
z = yield x + y
x = yield z * x
y = yield x + y + z
return y
try:
my_coro = simple_coroutine(5, 6)
print(next(my_coro))
print(my_coro.send(30))
print(my_coro.send(8))
print(my_coro.send('Done'))
except StopIteration as e:
print('执行完毕之后的值:', e.value)
运行结果
11
150
44
执行完毕之后的值: (8, 'Done', 30)
当预激活传入x = 5,y =6
时,第一次调用next()
当遇到yield
关键字,则交出函数的控制权,将yield
后面的表达式计算出并返回给next(my_coro)
中,所以当print(next(my_coro))
的时候,值是x + y = 11
,
第二步send(30)
即恢复函数的执行权,并将30赋值给第一次交出函数控制权的地方,即 z = yield x + y
处,此时send的值为30,则将z = 30
简单讲就是将 yield x + y
替换成 传入的值30,所以z = 30
继续执行,此时x = 5,y = 6 z= 30
遇到yield
关键字,交出函数的控制权,并计算yield
后面的表达式返回,此时表达式为z * x
,当前z = 30, x = 5
,所以计算出值为150
第三步,send(8)
恢复函数执行权,并将8赋值给上一次交出函数控制权的地方,将 8 赋值给 x ,此时x = 8,y = 6, z= 30
,继续运行程序,当遇到yield x + y +z
继续交出函数控制权,返回x + y + z
表达式的值44
第四步,send('Done')
继续恢复函数执行权,将Done 赋值给上一次交出函数控制权的地方,将Done 赋值给y ,此时x = 8, y = 'Done', z = 30
,继续执行,知道执行到return
处,整个控制流程结束,Python抛出StopIteration
异常,捕获异常可以得到return
的值(8, 'Done', 30)
运行过程
协程的运行过程中有4个状态
- GEN_CREATE:等待开始执行
- GEN_RUNNING:解释器正在执行,这个状态一般看不到
- GEN_SUSPENDED:在yield表达式处暂停
- GEN_CLOSED:执行结束
通过导入from inspect import getgeneratorstate
来获取协程状态
from inspect import getgeneratorstate
def simple_coroutine(x, y):
z = yield x + y
x = yield z * x
y = yield x + y + z
return (x, y, z)
try:
my_coro = simple_coroutine(5, 6)
print(getgeneratorstate(my_coro))
print(next(my_coro))
print(getgeneratorstate(my_coro))
print(my_coro.send(30))
print(my_coro.send(8))
print(my_coro.send('Done'))
except StopIteration as e:
print(getgeneratorstate(my_coro))
print('执行完毕之后的值:', e.value)
运行结果
GEN_CREATED
11
GEN_SUSPENDED
150
44
GEN_CLOSED
执行完毕之后的值: (8, 'Done', 30)
可以看到在未调用next()
方法时,协程的状态为GUN_CREATED
,在开始执行的时候协程的状态为GEN_SUSPENDED
,最后执行完毕之后状态为GEN_CLOSED
预激协程的装饰器
from functools import wraps
def coroutine(func):
@wraps(func)
def prime(*args, **kwargs):
gen = func(*args, **kwargs)
print(next(gen))
return gen
return prime
@coroutine
def simple_coroutine(x, y):
z = yield x + y
x = yield z * x
y = yield x + y + z
return (x, y, z)
try:
coro_arg = simple_coroutine(5, 6)
print(coro_arg.send(30))
print(coro_arg.send(8))
coro_arg.send(None)
except StopIteration as e:
print(e.value)
关于预激,在使用yield from句法调用协程的时候,会自动预激活,这样其实与我们上面定义的coroutine装饰器是不兼容的,在python3.4里面的asyncio.coroutine装饰器不会预激协程,因此兼容yield from
关于yield from
yield from
是在Python3.3才出现的语法。所以这个特性在Python2中是没有的
yield from
后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。
简单应用:拼接可迭代对象
使用yield
和使用yield from
的例子来对比
myStr = 'abc'
myList = [1, 2, 3]
mydict = {'name': 'aaron', 'age': '21'}
mygen = (i for i in range(4, 9))
def gen(*args):
for item in args:
for i in item:
yield i
newList = gen(myStr, myList, mydict, mygen)
print(list(newList))
# ['a', 'b', 'c', 1, 2, 3, 'name', 'age', 4, 5, 6, 7, 8]
myStr = 'abc'
myList = [1, 2, 3]
mydict = {'name': 'aaron', 'age': '21'}
mygen = (i for i in range(4, 9))
def gen(*args):
for item in args:
yield from item
newList = gen(myStr, myList, mydict, mygen)
print(list(newList))
# ['a', 'b', 'c', 1, 2, 3, 'name', 'age', 4, 5, 6, 7, 8]
由上面两种方式对比,可以看出,yield from
后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield
出来,对比yield
来说代码更加简洁,结构更加清晰
复杂应用:生成器的嵌套
当 yield from
后面加上一个生成器后,就实现了生成的嵌套。
当然实现生成器的嵌套,并不是一定必须要使用yield from
,而是使用yield from
可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现。
- 调用方:调用委派生成器的客户端(调用方)代码
- 委托生成器: 包含yield from表达式的生成器函数
- 子生成器: yield from后面加的生成器函数
# 委托生成器
def gen():
while True:
yield from averger_gen()
# 子生成器
def averger_gen():
total = 0
count = 0
averger = 0
while True:
averger = yield averger
total += averger
count += 1
averger = total / count
gen = gen()
next(gen)
print(gen.send(10))
print(gen.send(20))
print(gen.send(30))
委托生成器的作用是:在调用方与子生成器之间建立一个双向通道
。
调用方可以通过send()
直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方
委托生成器,只起一个桥梁作用,它建立的是一个双向通道
,它并没有权利也没有办法,对子生成器yield回来的内容做拦截。
# 委托生成器
def gen():
while True:
# 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。
total, count, averger = yield from averger_gen()
print('计算完成!总共计算:{}个,总和:{}分,平均分:{}'.format(count, total, averger))
# 子生成器
def averger_gen():
total = 0
count = 0
averger = 0
while True:
term = yield averger
if term is None:
break
total += term
count += 1
averger = total / count
# 每一次return,都意味着当前协程结束。
return total, count, averger
gen = gen()
next(gen)
print(gen.send(10))
print(gen.send(20))
print(gen.send(30))
gen.send(None) # 结束协程
运行结果
10.0
15.0
20.0
计算完成!总共计算:3个,总和:60分,平均分:20.0