第十六章 - 协程

协程

1、生成器用于生成供迭代的数据,协程是数据的消费者

2、to yield: 产出,让步

3、yield流程控制工具,使用它可以实现协作式多任务;协程可以把控制器让步给中心调度程序,从而激活其他的协程。

4、yield from实现协作式多线程

5、在Python3.5中 async替代asyncio.coroutine, await代替 yield from

6、async def定义协程,async for 定义异步迭代器

7、直接使用 .next .send方法比较low, 应该使用 asyncio模块来实现异步协程

8、

 

16.1 生成器如何进化成协程

生成器的调用方可以使用.send(...)方法发送数据,发送的数据会成为生成器函数中yield表达式的值,因此生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值(生产者消费者模型)。

注意:在Python3.3以前, 如果在生成器中给return语句提供值,会抛出SyntaxError异常。

 

16.2 用作协程的生成器的基本行为

协程有四个状态:

1、GEN_CREATED

2、GEN_RUNNING

3、GEN_SUSPENDED

4、GEN_CLOSED

因为send方法的参数会成为暂停的yield表达式的值,所以仅当协程处于暂停状态时才能调用send方法。

如果协程在GEN_CREATED状态,发送None之外的值给它,会出现以下报错:

def simple_coroutine():
    print("-> coroutine started")
    x = yield
    print("-> coroutine received:", x)

my_coro = simple_coroutine()

from inspect import getgeneratorstate
print(getgeneratorstate(my_coro))

next(my_coro)
print(getgeneratorstate(my_coro))
try:
    my_coro.send(23)
except StopIteration:
    pass
print(getgeneratorstate(my_coro))
>>>>
GEN_CREATED
-> coroutine started
GEN_SUSPENDED
-> coroutine received: 23
GEN_CLOSED

因此next(my_coro)函数这一步通常称为“预激”协程。

 

16.3 示例:使用协程计算移动平均值

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

coro_avg = averager()
next(coro_avg)  # 预激活协程
print(coro_avg.send(10))
print(coro_avg.send(30))
print(coro_avg.send(50))

>>>>
10.0
20.0
30.0

 

16.4 预激活协程的装饰器

from functools import wraps

def coroutine(func):
    """预激活协程装饰器,自动执行到第一个yield处使协程处于SUSPENDED状态"""
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer

使用@coroutine装饰器

from coroutil import coroutine

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

coro_avg = averager()
print(coro_avg.send(10))
print(coro_avg.send(30))
print(coro_avg.send(50))
>>>
10.0
20.0
30.0

 

16.5 终止协程和异常处理

协程中未处理的异常会向上冒泡,传给next函数或send方法的调用发(即触发协程的对象)。

从Python2.5开始,客户代码可以在生成器对象上调用两个方法,显示地把异常发给协程:

generator.throw(exc_type[, exc_value[, traceback]])

  致使生成器在暂停的yield表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式。而产出的值会成为调用generator.throw方法得到的返回值。

generator.close()

  致使生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器没有处理这个异常,或者抛出了StopIteration异常,调用方不会报错。

 

示例: generator.throw()

from inspect import getgeneratorstate

class DemoException(Exception):
    """测试"""

def demo_exc_handling():
    print("-> coroutine started")
    while True:
        try:
            x = yield
        except DemoException:
            print("*** DemoExcption handled. Continuing...")
        else:
            print("-> coroutine received: {!r}".format(x))
    raise RuntimeError("This line should never run.")


exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
exc_coro.throw(DemoException)
print(getgeneratorstate(exc_coro))
>>>>
-> coroutine started
-> coroutine received: 11
-> coroutine received: 22
*** DemoExcption handled. Continuing...
GEN_SUSPENDED

 

示例: generator.close()

from inspect import getgeneratorstate

class DemoException(Exception):
    """测试"""

def demo_exc_handling():
    print("-> coroutine started")
    while True:
        try:
            x = yield
        except DemoException:
            print("*** DemoExcption handled. Continuing...")
        else:
            print("-> coroutine received: {!r}".format(x))
    raise RuntimeError("This line should never run.")


exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
exc_coro.close()
print(getgeneratorstate(exc_coro))
>>>
-> coroutine started
-> coroutine received: 11
-> coroutine received: 22
GEN_CLOSED

 

16.6 让协程返回值

示例1: 无异常处理

from collections import namedtuple

Result = namedtuple("Result", "count, average")

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 Result(count, average)   # python3.3之前,如果生成器返回值,解释器会报语法错误


coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(20)
coro_avg.send(30)
coro_avg.send(None)
>>>>
StopIteration: Result(count=3, average=20.0)

 

示例2: StopIteration异常捕获

from collections import namedtuple

Result = namedtuple("Result", "count, average")

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 Result(count, average)   # python3.3之前,如果生成器返回值,解释器会报语法错误


coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(20)
coro_avg.send(30)
try:
    coro_avg.send(None)
except StopIteration as exc:
    result = exc.value

print(result)
>>>>
Result(count=3, average=20.0)  # 捕获StopIteration的到的结果

注意: yield from结构会在内部自动捕获StopIteration异常,这种处理方式与for循环处理StopIteration异常的方式一样。对于yield from结果来说, 解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from表达式的值。

 

16.7 使用yield from

在生成器中使用yield from subgen()时, subgen会获得控制权, 把产出的值传给gen的调用方,即调用方可以直接控制subgen, 与此同时, gen会阻塞,等待subgen终止。

def gen():
    yield from "ABC"

    yield from range(0, 10)

print(list(gen()))

yield from x 表达式对x 对象所做的第一件事是, 调用iter(x), 从中获取迭代器。

 

yield from的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者就可以直接发送和产出值。

PEP380中的术语:

委派生成器

  包含yield from <iterable> 表达式的生成器函数

子生成器

  从yield from表达式中<iterable>部分获取的生成器

调用方

  调用委派生成器的客户端代码

 

示例:

from collections import namedtuple
from coroutil import coroutine

Result = namedtuple("Result", "count average")


# 子生成器
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 Result(count, average)


# 委派生成器
@coroutine
def grouper(results, key):
    while True:
        results[key] = yield from averager()  # 一直等待averager()抛出StopIteration异常,然后再恢复运行(将StopIteration异常的第一个参数赋值给 results[key]),否则并且相当于一个中转站 将调用方的send来的数据,直接中转给averager。


# 客户端代码,调用方
def main(data):
    results = {}
    for key, values in data.items():
        group = grouper(results, key)
        for value in values:
            group.send(value)
        group.send(None)
    print(results)

data = {
    "girls;kg":
        [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    "girls;m":
        [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    "boys;kg":
        [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
    "boys;m":
        [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46]
}


if __name__ == '__main__':
    main(data)

>>>>
{'girls;m': Result(count=10, average=1.4279999999999997), 'girls;kg': Result(count=10, average=42.040000000000006), 'boys;m': Result(count=9, average=1.3888888888888888), 'boys;kg': Result(count=9, average=40.422222222222224)}

任何yield from链条都必须由客户驱动,在最外层委派生成器上调用next(...)函数或.sen(...)方法,可以隐式调用,例如使用for循环。

 

16.8 yield from的意义

“把迭代器当作生成器使用,相当于把子生成器的定义体内联在yield from表达式中。此外,子生成器可以执行return语句,返回一直值,而返回的值会成为yield from表达式的值。”

1、子生成器才产出的值都直接传给委派生成器的 --- 调用方

2、使用send()方法给委派生成器的值都直接传递给 子生成器。如果发送的值是None,那么会调用子生成器的__next__方法。如果发送的值不是None,那么会调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行。

3、生成器退出时,生成器中的return expr表达式会触发StopIteration。

4、yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。

5、传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法,如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。

6、如果把GeneratorExit异常传入委派生成器,或者在委派生成器上调用close()方法,那么会在子生成器上调用close()方法。

 

本章小结:

本章对协程的定义是宽泛的、不正式的,即:通过 调用方通过.send(...)方法发送数据或使用yield from结构驱动的生成器函数。

在asyncio库构建的协程采用更严格的方式:

  1、使用@asyncio.coroutine装饰器装饰

  2、始终使用yield from结构驱动,而不是通过直接在协程上调用.send(...)方法驱动

 

posted @ 2017-07-28 14:52  Vincen_shen  阅读(181)  评论(0)    收藏  举报