流畅python学习笔记:第十六章:协程

通常在python进行编程一般都是使用多线程或者多进程来实现。这里介绍另外一种并发的方式,就是协程,但和多线程以及多进程不一样的是,协程是运行在单线程当中的并发。来看下具体的例子:

def simple_coroutine():
    print 'corouting started'
   
x=yield   (1)
    print 'coroutine received %d'
% x

if __name__=="__main__":
    my_core=simple_coroutine()
    my_core.next()      (2)  也可以写成next(my_core)
    my_core.send(12)    (3)
执行结果如下:

E:python2.7.11python.exe E:/py_prj/fluent_python/chapter16.py

Traceback (most recent call last):

  File "E:/py_prj/fluent_python/chapter16.py", line 33, in <module>

    my_core.send(12)

StopIteration

corouting started

coroutine received 12

来看下代码的执行过程:

(1)x=yield为协程的表达式.x等于后面send发送进来的值。x是等于第三步中发送的值,也就是12.

(2)首先调用next函数。调用后生成器才能启动

(3)发送一个值,send方法会传入一个参数,该参数就是yield表达式的值,可以在生成器函数里面被接收。该参数协程定义体中的yield表达式会计算出12.现在协程处于工作状态,一直运行到下一个yield表达式,或者终止。

从这个执行过程来看,协程不是多线程的运行,而是单线程的运行。且运行方式和中断类。而且协程也有自身的状态总共有4个:1 GEN_CREATED 等待开始执行。2 GEN_RUNNING 解释器正在执行 3 GEN_SUSPEND 在yield表达式出暂停 4 GEN_CLOSED 执行结束。我们用getgeneratorstate来观测下状态的变化。通过getgeneratorstate(my_core)就可以得出具体的状态。但这个只限于python3才有,python2.7是没有的。

我们在来看另外一个协程的例子:计算移动平均值:

def averager():
    total=0.0
    count=0
    average=None
    while True:
        term=yield average    (1)
        total+=term
        count+=1
        average=total/count

if __name__=="__main__":
    core_average=averager()
    print core_average.next()   (a)
    print core_average.send(10) 
    print core_average.send(30)
    print core_average.send(5)
这是一个无限循环,也就是说只要调用方不停的发送值给这个协程。它就会一直收值。然后生成结果。我们来看下它的运行过程。
(1)  执行core_average.next的时候。执行到averager中的(1)位置,此时产生一个average,此时还没开始计算,因此产生的average值为None。此时协程在yield处暂停。等到调用发送值
(2)  core_average.send(10)激活协程,并且将10赋值给term。并更新total,count,average的值。然后开始while循环的下一次迭代。在yield处停止,产出average,此时average已经在上一次循环中被更新为10/1=10。然后继续等待激活协程
(3)  core_average.send(30) 激活协程,并且将20赋值给term。并更新total,count,average的值。然后开始while循环的下一次迭代。在yield处停止,产出average,此时average已经在上一次循环中被更新为(30+10)/2=20。然后继续等待激活协程
(4)  core_average.send(5) 激活协程,执行步骤和之前一样,此时average=(10+30+5)/3=15
最终的执行结果如下,符合我们的预期
E:python2.7.11python.exe E:/py_prj/fluent_python/chapter16.py
None
10.0
20.0
15.0

那么协程是如何停止的,前面的simple_coroutine例子在结束后会抛出StopIteration异常。那么在averager这个无线循环中如何退出呢。来看下下面的这个调用:

core_average=averager()
print core_average.next()
print core_average.send(10)
print core_average.send('abc')
将core_average.send(30)改成core_average.send('abc'),也就是发送一个字符,而不是发送一个整数。这时候的执行结果会产生一个异常而退出。证明了协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方,也就是触发协程的对象。
Traceback (most recent call last):
  File "E:/py_prj/fluent_python/chapter16.py", line 44, in <module>
    print core_average.send('abc')
  File "E:/py_prj/fluent_python/chapter16.py", line 36, in averager
    total+=term
TypeError: unsupported operand type(s) for +=: 'float' and 'str'

基于这个实现,python2.5后可以调用两个方法,将异常发给协程。这个两个方法是throw和close

使用throw方法与协程通信:

一旦协程开启,仅仅通过send与yield只能对协程进行简单的控制。throw方法提供了在协程内引发异常的接口。通过主叫者调用throw在协程内引发异常,协程捕获异常的方式,可以实现主叫者与协程之间的通信。

需要注意的是,使用throw方法在协程内引发的异常,如果没有被捕获,或者内部又重新raise了不同的异常,那么这个异常会传播到主叫者。

同时throw方法同send与next一样,都会使协程继续运行,并返回下一个yield表达式中的表达式值。且同样的,如果不存在下一个yield表达式协程就结束了,主叫方会收到StopIteration Exception。

 

使用close方法来结束协程

close方法与throw方法类似,都是在协程暂停的位置引发一个异常,从而向协程发出控制信息。不同于throw方法可以抛出自定义异常,close方法会固定抛出GeneratorExit异常。当这个异常没有被捕获或者引发StopIteration Exception时,close方法会正常返回。这个比较好理解,协程设计者捕获了GeneratorExit异常并完成清理,保证清理干净了,所以最后不会再有yield返回值引发StopItertation。如果因为设计错误导致仍然有下一个yield,那么就会抛出RuntimeError.

来看下close的使用:

core_average=averager()
print core_average.next()
print core_average.send(10)
print core_average.send(30)
core_average.close()
print core_average.send(5)
在执行core_average.send(5)的时候抛出了StopIteration。因为在前一步执行了core_average.close()方法停止了协程

Traceback (most recent call last):

  File "E:/py_prj/fluent_python/chapter16.py", line 46, in <module>

    print core_average.send(5)

StopIteration

None

10.0

20.0

我们来看一个更加具体的例子:一个关于生产者和消费者的例子,传统的生产者和消费者的是一个线程写消息,一个线程取消息,通过锁机制控制,但一不小心就会死锁,我们用协程的方式来改造下。
def consumer():
    r=''
    while
True:
        n=yield r
        if not n:
            return
        print 'Consumer consuming....%d'
% n
        time.sleep(1)
        r='OK'

def
producer(c):
    c.next()
    n=0
    while n<5:
        n=n+1
        print 'Producer producing....%d' % n
        r=c.send(n)
        print 'Consumer return %s' % r
    c.close()
if __name__=="__main__":
    c=consumer()
    producer(c)
E:python2.7.11python.exe E:/py_prj/fluent_python/chapter16.py
Producer producing....1
Consumer consuming....1
Consumer return OK
Producer producing....2
Consumer consuming....2
Consumer return OK
Producer producing....3
Consumer consuming....3
Consumer return OK
Producer producing....4
Consumer consuming....4
Consumer return OK
Producer producing....5
Consumer consuming....5
Consumer return OK
整个过程如下:
  1. 首先调用c.next()启动生成器;
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;在这里c.send(n),其中n值就传递给了consumer中的n,(n=yield r). 因此yield左边的值代表接受的值,右边为返回的值
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;返回的值是consumer中的r
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

yield from:

python3中,引入了yield from的语法结构。在生成器gen中使用yield from subgen()时,subgen会获得控制权,把产出的值传给gen的调用方。也就是说调用方可以直接控制subgen,同时gen会被阻塞,等待subgen终止。来看下yieldyield from的使用区别

def gen():

    for c in 'AB':

        yield c

    for i in range(1,3):

        yield i

def gen1():

    yield from 'AB'

yield from range(1,3)

在这里我们可以看到使用yiled fromyield更简洁。而yield from ‘AB’其实就是代替了for c in ‘AB’: yield c的作用。那么从这里总结出yield from的第一个特征:yield from后面是一个迭代器。也称为子生成器。在这里gen1被称为委派生成器。

调用关系如下图所示:

yield from的作用就是将最外层的调用方与最内层的子生成器连接起来。这样二者就可以直接发送和产出值。而中间的这个桥梁或者是管道就是委派生成器。我们先不看图片中的例子,我们来看个我们之前的例子。

在http://www.cnblogs.com/zhanghongfeng/p/7301675.html这篇帖子中,我们利用yield实现了一个生产,消费的模型。下面我们用yield from来改造这个程序

def consumer():

    r=''

    while True:

        n=yield r

        if not n:

            return

        print('Consumer consuming....%d' % n)

        time.sleep(1)

        r='OK'

def product():

    ret=yield from consumer()

    print('Consumer return %s' % ret)

if __name__=="__main__":

    p=product()

    n=0

    next(p)

    while n<5:

        n+=1

        print('Product producing....%d' % n)

        p.send(n)

通过yield from我们可以将代码精简。在上面的代码中,product就是委派生成器,consumer就是子生成器。p.send(n)将生产的数据发给消费者。从而使得消费者进行消费。

下面来看一个通过生成器模仿的出租车运营的代码:

Event=collections.namedtuple('Event','time proc action')

def taxi_process(ident,trips,start_time=0):

    time=yield Event(start_time,ident,'leave garage')

    for i in range(trips):

        time=yield Event(time,ident,'pick up passenger')

        time=yield Event(time,ident,'drop off passenger')

    yield Event(time,ident,'going home')

if __name__=="__main__":

    taxi=taxi_process(ident=13,trips=2,start_time=0)

    next(taxi)

    ret=taxi.send(7)

    print(ret)

    ret =taxi.send(23)

    print(ret)

    ret =taxi.send(5)

    print(ret)

    ret =taxi.send(48)

    print(ret)

    ret =taxi.send(1)

print(ret)

(1)首先创建一个生成器对象,表示一辆出租车,这辆出租车的编号是13,t=0的时候开始工作

(2) next(taxi)产生第一个事件。然后发送当前时间,这里在时间上加7,意思是这辆出租车7分钟后找到第一个乘客

(3)for循环产生了的一个行程

(4)发送时间23,表示第一个乘客的形成持续了23分钟

(5)依次进行上面的流程,直到两次行程完成后,for循环结束,产出’going home’事件

(6)如果再尝试把值发给协程,会执行到协程的末尾,协程返回后,解释器抛出StopIteration.

原文地址:https://www.cnblogs.com/zhanghongfeng/p/7301675.html