Python协程详解(一)
yield有两个意思,一个是生产,一个是退让,对于Python生成器的yield来说,这两个含义都成立。yield这个关键字,既可以在生成器中产生一个值,传输给调用方,同时也可以从调用方那获取一个值,在生成器内部使用。此外,yield还会作出让步,暂停生成器,让调用方继续工作,直到调用方需要下一个数据时,调用方则陷入等待直到成器提供给调用方所需的数据,如此循环往复。乍一听,有点像多线程,不明白多线程的同学也不要紧张,可以简单的解释一下多线程
解释多线程之前,我们先解释一下进程,进程可以看成是电脑里运行的一个实例,比方说,我运行一个浏览器,是一个进程,运行QQ,同样也是进程,我用浏览器浏览网站,用浏览器听音乐和下载东西,可以看成浏览器这个进程,里面有3个线程同时在为我做浏览网站,播放音乐还有下载文件。而我用QQ和人聊天,同时我又用QQ给人传输文件,同样也是在QQ这个进程中,有两个线程在为我传输聊天内容,同时传输文件。当然,上述说的并不严谨,只是为了好理解,因为对于像浏览器或者QQ这样的进程,每时每秒可能有成百上千的线程在运行,有可能记录日志或者其他
而协程相比于线程,最大的区别在于,协程不需要像线程那样来回的中断切换,也不需要线程的锁机制,因为线程中断或者锁机制都会对性能问题造成影响,所以协程的性能相比于线程,性能有明显的提高,尤其在线程越多的时候,优势越明显
下面用个例子来看一下协程的运作:
def simple_coroutine(): for i in range(3): x = yield i + 1 # <1> print("从调用方获取的值:%s" % x) my_coro = simple_coroutine() # <2> first = next(my_coro) # <3> for i in range(5): # <4> try: y = my_coro.send(i) # <5> print("从生成器中获取的值:%s" % y) except StopIteration: print("生成器的值拉取完毕") # <6> print("生成器最初获取的值:%s" % first)
运行结果:
从调用方获取的值:0 从生成器中获取的值:2 从调用方获取的值:1 从生成器中获取的值:3 从调用方获取的值:2 生成器的值拉取完毕 生成器的值拉取完毕 生成器的值拉取完毕 生成器最初获取的值:1
我们先来说一下程序的运行过程,先看程序中<2>处的代码,传统的概念中,我先执行了my_coro = simple_coroutine() 这块代码,所以会理所当然的认为, simple_coroutine() 这个方法要先执行完毕才能接着执行后续的代码,但实际上不是,因为yield会标明这个方法是一个生成器,所以在程序的最初,他不会先执行完毕 simple_coroutine() 方法,而是把my_coro 这个变量声明称一个生成器,跳过simple_coroutine() ,接着执行后续代码
Python将my_coro 声明称一个生成器后,调用了<3>处的next(my_coro) ,这个方法才开始会顺序执行simple_coroutine()方法中的代码,在Python解释器执行simple_coroutine()方法时,遇到yield关键字,生成器将会陷入等待,这时候解释器会跳到生成器之外,也就是<3>之后的代码,我们调用生成器的send()方法,并传输一个值,这时候Python解释器会从外部的代码重新跳回simple_coroutine() 方法,中在之前停留的地方继续执行
他会将外部传来的值赋给x变量,并顺序执行,直到遇到下一个yield,再像之前那样跳出方法外
由于生成器能提供的值有限,所以当simple_coroutine()方法中执行了3次循环,生成器已经没有多余的值可供调用方获取了,所以每次调用生成器的send()方法,都会抛出StopIteration异常
这里有一点要注意,要激活一个生成器,一定要调用next()方法,而不是调用生成器的send()方法,如果直接调用send()方法会报错
协程有四种状态,分别是
GEN_CREATED:等待执行
GEN_RUNNING:解释器执行
GEN_SUSPENDED:在yield表达式处暂停
GEN_CLOSED:执行结束
协程的状态可以用inspect.getgeneratorstate()函数来确定,来看下面的例子:
from inspect import getgeneratorstate from time import sleep import threading def get_state(coro): print("其他线程生成器状态:%s", getgeneratorstate(coro)) # <1> def simple_coroutine(): for i in range(3): sleep(0.5) x = yield i + 1 # <1> my_coro = simple_coroutine() print("生成器初始状态:%s" % getgeneratorstate(my_coro)) # <2> first = next(my_coro) for i in range(5): try: my_coro.send(i) print("主线程生成器初始状态:%s" % getgeneratorstate(my_coro)) # <3> t = threading.Thread(target=get_state, args=(my_coro,)) t.start() except StopIteration: print("生成器的值拉取完毕") print("生成器最后状态:%s" % getgeneratorstate(my_coro)) # <4>
执行结果:
生成器初始状态:GEN_CREATED 生成器状态:%s GEN_SUSPENDED 生成器状态:%s GEN_SUSPENDED 生成器的值拉取完毕 生成器的值拉取完毕 生成器的值拉取完毕 生成器最后状态:GEN_CLOSED
<2>处,在激活协程之前,协程的状态是GEN_CREATED,而执行next()之后,以及在调用生成器send()之间,我分主线程也就是调用方和多线程去观察协程的状态,结果状态都是GEN_SUSPENDED,也就是协程处于暂停的状态,我原本想用多线程去捕捉协程的运行态,结果即便是多线程捕捉协程也是GEN_SUSPENDED,而GEN_RUNNING也说明,只有带解释器在运行协程的时候,协程的状态才是GEN_RUNNING,最后是GEN_CLOSED,我们拉取完协程的值后,协程的状态就变为执行结束
示例:使用协程计算平均值
我们可以开发一个协程,不断的往协程发送值,并且让协程累计之前的值并计算平均值,如下:
from functools import wraps def coroutine(func): @wraps(func) def primer(*args, **kwargs): gen = func(*args, **kwargs) next(gen) return gen return primer @coroutine # <1> def averager(): total = .0 count = 0 average = None while True: term = yield average total += term count += 1 average = total / count try: coro_avg = averager() print(coro_avg.send(10)) print(coro_avg.send(20)) print(coro_avg.send(30)) coro_avg.close() # <2> print(coro_avg.send(40)) except StopIteration: print("协程已结束")
运行结果:
10.0 15.0 20.0 协程已结束
在<1>处,我们用一个装饰器来预先激活协程,而不是之后再调用方里执行一个next()函数。然后,我们不断往协程里传10、20、30,而协程不断累计传入的值,并计算所有值的平均值返回给调用方,最后,我们在<2>处调用协程的close()函数,关闭协程,再调用send()方法,会发现抛出StopIteration异常
当发送给协程不是数字,会导致协程内部有异常抛出
for i in range(1, 6): try: print(coro_avg.send(i)) if i % 3 == 0: coro_avg.send('') except StopIteration: print("协程已结束") except TypeError: print("传入值异常")
运行结果:
1.0 1.5 2.0 传入值异常 协程已结束 协程已结束
我们设置,当i为3的时候,多传入一个空字符串,结果协程抛出类型错误,协程将运行状态改为结束,之后再往协程传值,都抛出StopIteration异常
我们可以让协程处理一些特定的异常,比如:
class DemoException(Exception): # <1> pass def demo_exec_handling(): print("coroutine started") while True: try: x = yield # <2> except DemoException: # <3> print("DemoException handled") else: print("coroutine received:{}".format(x))
运行结果
>>> exec_coro = demo_exec_handling() >>> next(exec_coro) coroutine started >>> print(exec_coro.send(1)) coroutine received:1 None >>> exec_coro.send(2) coroutine received:2 >>> exec_coro.send(3) coroutine received:3 >>> exec_coro.throw(DemoException) DemoException handled >>> exec_coro.send(4) coroutine received:4 >>> exec_coro.throw(ZeroDivisionError) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in demo_exec_handling ZeroDivisionError >>> exec_coro.send(5) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
<1>处的DemoException是用来测试协程的,首先我们可以看到,当yield右边没有任何式子时,返回给调用方的是一个None对象,其次如果我们调用throw()方法将异常传入协程,因为协程里有关于DemoException的捕捉,所以协程会继续执行,当我们继续传入ZeroDivisionError,则协程结束
让协程返回值
我们可以改造之前的averager()函数,使它可以返回一个对象,对象里有count和average两个属性
from collections import namedtuple Result = namedtuple("Result", ["count", "average"]) def averager(): total = .0 count = 0 average = None while True: term = yield average if term is None: break total += term count += 1 average = total / count return Result(count, average) 运行结果: >>> coro_avg = averager() >>> next(coro_avg) >>> coro_avg.send(10) 10.0 >>> coro_avg.send(20) 15.0 >>> coro_avg.send(30) 20.0 >>> coro_avg.send(None) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration: Result(count=3, average=20.0)
当我们发送None的时候,协程结束,返回结果,一如既往,生成器会抛出StopIteration异常,异常对象的value属性保存着返回值,为了获取返回值,我们还要再修改一下代码
try: coro_avg = averager() next(coro_avg) coro_avg.send(10) coro_avg.send(20) coro_avg.send(30) coro_avg.send(None) except StopIteration as exc: result = exc.value print(result)
运行结果:
Result(count=3, average=20.0)
结语:关于协程yield结构这一块,到此暂做结束,下一章会介绍协程的yield from结构,yield from结构会在内部自动捕获StopIteration异常,还会把协程的返回值变成yield from表达式的值,下一章节将会讨论yield from的结构和用法,谢谢大家