《流畅的Python》笔记。python
本篇主要讨论一个与生成器看似无关,但实际很是相关的概念:协程。微信
说到协程(Coroutine),若是是刚接触Python不久的新手,估计第一个反应是:懵逼,这是个什么玩意儿?有一点基础的小伙伴可能会想到进程和线程。多线程
其实,和子程序(或者说函数)同样,协程也是一种程序组件。Donald Knuth曾经说过,子程序是协程的特例。咱们都知道,一个子程序就是一次函数调用,它只有一个入口和一个出口:调用者调用子程序,子程序运行完毕,将结果返回给调用者。而协程则是多入口和多出口的子程序:调用者能够不止一个,执行过程当中能够暂停,输出结果也能够不止一个。闭包
协程和进程、线程也是有关系的:为了实现并发,高效利用系统资源,因而有了进程;为了实现更高的并发,以及减少进程切换时的上下文开销,因而有了线程;但即使线程切换时的开销小了,若是线程数量一多(好比10K个),这时的上下文切换也不可小觑,因而在线程中加入了协程(这里之因此是“加入”,是由于协程的概念出现得比线程要早)。协程运行在一个线程当中,不会发生线程的切换,而且,它的启停能够由用户自行控制。因为协程在一个线程中运行,因此在共享资源时不须要加锁。并发
补充:之后有机会单独出一篇详细介绍进程、线程和协程的文章。函数
这三者本不该该放在一块儿,之因此放在一块儿,是由于生成器将迭代器和协程联系了起来,或者说yield
关键字将这三者联系了起来:生成器能够做为迭代器,生成器又是协程比不可少的组成部分。但千万不要把迭代器用做协程,也别把协程用做迭代器!这二者并不该该存在关系。学习
yield
关键字背后的机制很强大,它不只能向用户提供数据,还能从用户那里获取数据。而迭代器、生成器和协程这三个概念实际上是对yield
关键字用法的取舍:网站
yield
或者yield from
的函数都是生成器,无论你是用来干啥;yield
来生成数据,或者说向用户提供数据,那么这个生成器能够看作迭代器(用做迭代器的生成器);yield
来获取外部的数据,实现双向数据交换,那么这个生成器可看作协程(用做协程的生成器)。这里先列举出迭代器和协程在代码上最直观的区别:spa
def my_iter(): # 用做迭代器的生成器
yield 1; # 做为迭代器,yield关键字后面会跟一个数据
yield 2; # 且不关心yield的返回值,没有赋值语句
def my_co(): # 用做协程的生成器
x = yield # 这种写法表示但愿从用户处获取数据,而不向用户提供数据(其实提供的是None)
y = yield 1 # 这种写法表示既向用户提供数据,也但愿获得用户的反馈
复制代码
本节主要包括协程的运行过程,协程的4个状态,协程的预激,协程的终止和异常处理,协程的返回值。.net
协程自己有4个状态(其实就是生成器的4个状态),可使用inspect.getgeneratorstate()
函数来肯定:
GEN_CREATED
:等待开始执行;GEN_RUNNING
:解释器正在执行,多线程时能看到这个状态;GEN_SUSPENDED
:在yield
表达式处暂停时的状态;GEN_CLOSED
:执行结束。下面经过一个简单的例子来讲明这四个状态以及协程的运行过程:
>>> def simple_coro(a):
... print("Started a =", a)
... b = yield a
... print("Received b =", b)
... c = yield a + b
... print("End with c=", c)
...
>>> from inspect import getgeneratorstate
>>> my_coro = simple_coro(1)
>>> getgeneratorstate(my_coro)
'GEN_CREATED' # 刚建立的协程所处的状态,这时协程尚未被激活
>>> next(my_coro) ### 第一次调用next()叫作预激,这一步很是重要! ###
Started a = 1
1
>>> >>> getgeneratorstate(my_coro)
'GEN_SUSPENDED' # 在yield表达式处暂停时的状态
>>> my_coro.send(2) # 经过.send()方法将用户的数据传给协程
Received b = 2
3
>>> my_coro.send(3)
End with c= 3
Traceback (most recent call last):
File "<input>", line 1, in <module>
StopIteration # 协程(生成器)结束,抛出StopIteration
>>> getgeneratorstate(my_coro)
'GEN_CLOSED' # 协程结束后的状态
复制代码
解释:
next()
调用就是预激,这一步很是重要,它将运行到第一yield
表达式处并暂停。对于没有预激的协程,在调用.send(value)
时,若是value
不是None
,解释器将抛出异常。对于预激,既能够调用next()
函数,也能够.send(None)
(此时会被特殊处理)。但对于yield from
来讲则不用预激,它会自动预激。.send()
方法实现了用户和协程的交互。yield
是一个表达式(上述代码中等号的右边),它的默认返回值是None
,若是用户经过.send(value)
传入了参数value
,那么这个值将做为协程暂停处的yield
表达式的返回值。next()
函数或.send()
方法时,协程会运行到下一个yield
表达式处并暂停。具体来讲,好比上述代码中的b = yield a
,代码实际上是停在等号的右边,yield a
这个表达式尚未返回,只是把a
传给了用户,但尚未计算出yield a
表达式的返回值,b
所以也没有被赋值。当代码再次运行时,等号右边的yield a
表达式才返回值,并将这个值赋给b
。若是经过next()
函数让协程继续运行,则上一个暂停处的**yield
表达式将返回默认值**None
(b = None
);若是经过.send(value)
让协程继续运行,则上一个yield
表达式将返回value
(b = value
)。这也解释了为何要预激协程:若是没有预激,也就没有yield
表达式与传入的value
相对应,天然也就抛出异常。协程中没处理的异常会向上冒泡,传给next()
函数或.send()
方法的调用方。不过,咱们也能够经过.throw()
方法手动抛出异常,还能够经过.close()
方法手动结束协程:
generator.throw(exc_type[, exc_value[, traceback]])
:让生成器在暂停的yield
表达式处抛出指定的异常。若是生成器处理了这个异常,代码会向前执行到下一个yield
表达式yield a
,并将生成的a
做为generator.throw()
的返回值。若是生成器没有处理抛出的异常,则会向上冒泡,而且生成器会终止,状态转换成GEN_CLOSED
。generator.close()
:使生成器在暂停处的yield
表达式处抛出GeneratorExit
异常。若是生成器没有处理这个异常,或者处理时抛出了StopIteration
异常,.close()
方法直接返回,且不报错;若是处理GeneratorExit
时抛出了非StopIteration
异常,则向上冒泡。从上一篇和本篇的代码中,不知道你们发现了一个现象没有:全部的生成器最后都没有写return
语句。这实际上是有缘由的,由于在Python3.3以前,若是生成器返回值,解释器会报语法错误。如今则不会报错了,但返回的值并非像普通函数那样能够直接接收:Python解释器会把这个返回值绑定到生成器最后抛出的StopIteration
异常对象的value
属性中。示例以下:
>>> def test():
... yield 1
... return "This is a test"
...
>>> t = test()
>>> next(t)
1
>>> next(t)
Traceback (most recent call last):
File "<input>", line 1, in <module>
StopIteration: This is a test # StopIteration有了附加信息
>>> t = test()
>>> next(t)
1
>>> try:
... next(t)
... except StopIteration as si:
... print(si.value) # 获取返回的值
...
This is a test
复制代码
从前文咱们知道,若是要使用协程,必需要预激。能够手动经过调用next()
函数或者.send(None)
方法。但有时咱们会忘记手动预激,此时,咱们可使用装饰器来自动预激协程,这个装饰器以下:
from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen
return primer
复制代码
提早预激的生成器只能和yield
兼容,不能和yield from
兼容,由于yield from
会自动预激。因此请肯定你的生成器要不要被放在yield from
以后。
上一篇文章说到,对于嵌套生成器,使用yield from
能减小不少代码,好比:
def y2():
def y1(): # y1只要是个可迭代对象就行
yield 1
yield 2
# 第一种写法
for y in y1():
yield y
# 第二种写法
# yield from y1()
if __name__ == "__main__":
for y in y2():
print(y)
复制代码
第二种写法明显比第一种简洁。这是yield from
的一个做用:简化嵌套循环。yield from
后面还能够跟任意可迭代对象,并非只能跟生成器。
yield from
最重要的做用是起到了相似通道的做用:它能让客户端代码和子生成器之间进行数据交换。
这里有几个术语须要先解释一下:
yield from <iterable>
表达式的生成器函数。<iterable>
部分就是子生成器。<iterable>
也能够是委派生成器,以此类推下去,造成一个链条,但这个链条最终以一个只使用yield
表达式的简单生成器结束。好比上述代码,按照没有yield from
语句的写法,若是客户端代码想经过y2.send(value)
向y1
传值,value
只能传到y2
这一层,若是想再传入y1
,将要写大量复杂的代码。下面是yield from
的说明图:
结合上图,可作以下总结:
yield from
和yield
在使用上并没有太大区别;next()
或.send(None)
时,委派生成器会执行到第一个yield from
表达式并暂停。当客户端继续调用委派生成器的.send()
,.throw()
和.close()
等方法时,会“直接”做用到最内层的子生成器上,而不是让委派生成器的代码继续向前执行。只有当子生成器抛出StopIteration
异常后,委派生成器中的代码才继续执行,并将StopIteration.value
的值做为yield from
表达式的返回值。这一小节是yield from
的逻辑伪代码实现,代码较为复杂,看不懂也没什么关系,能够跳过,也可直接看最后的总结,并不影响yield from
的使用。
### "RESULT = yield from EXPR"语句的等效代码
_i = iter(EXPR) # 获得EXPR的迭代器
try:
_y = next(_i) # 预激!尚未向客户端生成值
except StopIteration as _e: # 若是_i抛出了StopIteration异常
_r = _e.value # _i的最后的返回值。这不是最后的生成值!
else: # 若是调用next(_i)一切正常
while 1: # 这是一个无限循环
try:
_s = yield _y # 向客户端发送子生成器生成的值,而后暂停
except GeneratorExit as _e: # 若是客户端调用.throw(GeneratorExit),或者调用close方法
try: # 首先尝试获取_i的close方法,由于_i不必定是生成器,普通迭代器不会实现close方法
_m = _i.close
except AttributeError:
pass # 没有获取到close方法,什么也不作
else:
_m() # 若是获取到了close方法,则调用子生成器的close方法
raise _e # 最后无论怎样,都向上抛出GeneratorExit异常
except BaseException as _e: # 若是客户端经过throw()传入其它异常
_x = sys.exc_info() # 获取错误信息
try: # 尝试获取_i的throw方法,理由和上面的状况同样
_m = _i.throw
except AttributeError: # 若是没有这个方法
raise _e # 则向上抛出用户传入的异常
else: # 若是_i有throw方法,即它是一个子生成器
try:
_y = _m(*_x) # 尝试调用子生成器的throw方法
except StopIteration as _e:
_r = _e.value # 若是子生成器抛出StopIteration,获取返回的值
break # 而且跳出循环
else: # 若是在生成器生成值时没有异常发生
try: # 试验证用户经过.send()方法传入的值
if _s is None: # 若是传入的是None
_y = next(_i) # 则尝试调用next(),向前继续执行
else: # 若是传入的不是None,则尝试调用子生成器的send方法
_y = _i.send(_s)
# 若是子生成器没有send方法,则向上报AttributeError
except StopIteration as _e: # 若是子生成器抛出了StopIteration
_r = _e.value # 获取子生成器返回的值
break # 并跳出循环,回复委派生成器的运行
RESULT = _r # _r就是yield from EXPR最终的返回值,将其赋予RESULT
复制代码
从上面这么长一串代码能够看出,若是没有yield from
,而咱们又想向最内层的子生成器传值,这得多麻烦。下面总结出几点yield from
的特性:
.send(value)
将值传给委派生成器时,若是value
是None
,则调用子生成器的__next__
方法;不然,调用子生成器的.send(value)
;.throw()
,委派生成器会先肯定子生成器有没有.throw()
方法,若是有,则调用,若是没有,则向上抛出AttributeError
异常;.throw(GeneratorExit)
或者.close()
方法时,委派生成器也会先肯定子生成器有没有.close()
方法,若是有,则调用子生成器的.close()
方法,由子生成器来抛出GeneratorExit
异常,委派生成器将这个异常向上传递;若是子类没有.close()
方法,则委派生成器直接抛出GeneratorExit
异常。Python解释器会捕获这个异常,但不会显示异常信息。StopIteration
异常,不论是用户经过.throw
方法传递的,仍是子生成器运行结束时抛出的,都会致使委派生成器继续向前执行。在《Python学习之路26》中,咱们分别用类和闭包来实现了平均值的计算,如今,做为本章最后一个例子,咱们使用协程来实现平均值的计算,其中还会用到yield from
和生成器的返回值:
import inspect
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 average
def grouper(results, key):
while True: # 每一个循环都会新建averager
results[key] = yield from averager()
def main(data):
results = {}
for key, values in data.items():
group = grouper(results, key) # 每一个循环都会新建grouper
next(group) # 激活
for value in values:
group.send(value)
# 此句很是重要,不然不会执行到averager()中的return语句,也就得不到最终的返回值
group.send(None)
print(results)
data = {"list1": [1, 2, 3, 4, 5], "list2": [6, 7, 8, 9, 10]}
if __name__ == "__main__":
main(data)
# 结果:
{'list1': 3.0, 'list2': 8.0}
复制代码
不知道你们看到这段代码的时候有没有什么疑问。当笔者看到grouper()
委派生成器里的While True:
时,很是疑惑:为啥要加个While
循环呢?若是按这个版本,咱们在main
中的for
循环后检测group
的状态,会发现它是GEN_SUSPENDED
,这笔者的强迫症就犯了,怎么能不是GEN_CLOSED
呢?!并且这个版本每当执行完group.send(None)
后,在grouper()
中又会建立新的averager
,而后当main
中group
更新后,上一个grouper
(也就是刚新建了averager
的grouper
)因为引用数为0,又被回收了。刚新建一个averager
就被回收,这很少此一举吗?因而笔者将代码改为了以下形式:
def grouper(results, key): # 去掉了循环
results[key] = yield from averager()
def main(data):
results = {}
for key, values in data.items():
-- snip --
try: # 手动捕获异常
group.send(None)
except StopIteration:
continue
复制代码
写出来后发现代码并无以前的简洁,但至少group
最后变成了GEN_CLOSED
状态。至于最后怎么取舍就看各位了。
迎你们关注个人微信公众号"代码港" & 我的网站 www.vpointer.net ~