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的结构和用法,谢谢大家

 

posted @ 2018-05-20 22:43  北洛  阅读(7194)  评论(0编辑  收藏  举报