什么是协程

协程

to yield 含义:产出和让步。

yield item这行代码会产出一个值,提供给next(...)的调用方;此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next()。调用方会从生成器中拉取值。

从根本上把yield视作控制流程的方式,这样就好理解协程了。

将生成器当作一个协程

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

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

my_coro = simple_coroutine()
next(my_coro)  # ②
my_coro.send(42) # ③

# 输出:
->coroutine started
Traceback (most recent call last):
-> coroutine received: 42
  File "C:/Users/42072/PycharmProjects/day01/ready04/yield_reday.py", line 62, in <module>
    my_coro.send(42)
StopIteration
  • ① yield在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是None——这个值是隐式指定的,因为yield关键字右边没有表达式。
  • ② 首先要调用next(...)函数,因为生成器还没启动,没在yield语句处暂停,所以一开始无法发送数据。
  • ③ 调用这个方法后,协程定义体中的yield表达式会计算出42;现在,协程会恢复,一直运行到下一个yield表达式,或者终止。

协程的状态

  1. GEN_CREATED:等待开始执行
  2. GEN_RUNNING:解释器正在执行
  3. GEN_SUSPENDED:在yield表达式处暂停
  4. GEN_CLOSED:执行结束
from inspect import getgeneratorstate

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

my_coro = simple_coroutine()
print(getgeneratorstate(my_coro))
next(my_coro)
print(getgeneratorstate(my_coro))
my_coro.send(42)
print(getgeneratorstate(my_coro))

注意:

  • 因为send方法的参数会成为暂停的yield表达式的值,所以,仅当协程处于暂停状态时才能调用send方法。
  • 如果给未激活的协程对象发送None以外的值,会引发错误。

激活协程的方式有两种:

  • next(my_coro)方法
  • my_coro.send(None)

示例:协程产出两个值

def simple_coro2(a):
    print('->Started :a = ' ,a)
    b = yield a
    print('->Started :b = ' ,b)
    c = yield a + b
    print('->Received: c=',c)

my_coro2 = simple_coro2(14)
next(my_coro2)
rs1 = my_coro2.send(28)
print(rs1)
rs2 = my_coro2.send(99)
print(rs2)

案例:计算移动平均值

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(20))
print(coro_avg.send(30))
#输出:
10.0
15.0
20.0

使用协程之前必须预激,可是这一步容易忘记。为了避免忘记,可以在协程上使用一个特殊的装饰器。

  • 额外知识:wraps
作者:hqzxsc2006 
来源:CSDN 
原文:https://blog.csdn.net/hqzxsc2006/article/details/50337865 

Python装饰器(decorator)在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响,Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。写一个decorator的时候,最好在实现之前加上functools的wrap,它能保留原有函数的名称和docstring。

#不加wraps
def my_decorator(func):
    
    def wrapper(*args, **kwargs):
        """decorator"""
        print("Calling decorated function...")
        return func(*args,**kwargs)

    return wrapper

@my_decorator
def example():
    """Docstring"""
    print('Called example function')
    
print(example.__name__,example.__doc__)

#输出:
wrapper decorator


#加wraps

from functools import wraps
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """decorator"""
        print("Calling decorated function...")
        return func(*args,**kwargs)

    return wrapper

@my_decorator
def example():
    """Docstring"""
    print('Called example function')
    
print(example.__name__,example.__doc__)

#输出:
example Docstring

预激协程

协程如果不激活,那么则没什么用。调用send()方法之前,需要先调用next(my_coro)方法进行激活。为了简化协程的用法,有时会使用一个预激装饰器。

from functools import wraps


def coroutine(func):
    @wraps(func)
    def primer(*args,**kwargs):
        gen = func(*args,**kwargs)
        next(gen)
        return gen

    return primer


@coroutine
def averager():
    total = 0.0
    count = 0
    average = None

    while True:
        term = yield average
        total += term
        count += 1
        average = total / count

from inspect import getgeneratorstate
coro_avg = averager()
print(getgeneratorstate(coro_avg))
print(coro_avg.send(10))
print(coro_avg.send(30))

输出:
GEN_SUSPENDED
10.0
20.0

如果使用yield from 语法,会自动预激。asyncio.coroutine装饰器不会预激活协程,因此能兼容yield from 句法。

终止协程和异常处理

协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方。

例如:

coro_avg = averager()
print(coro_avg.send(20))
print(coro_avg.send('aa'))

  • 由于第三行发送的不是数据,导致协程内部抛出异常。
  • 由于协程内部没有处理异常,协程会终止。如果试图重新激活协程,会抛出StopIteration异常。

python 2.5以后,客户代码可以调用以下两个方法,显示地把异常发给协程:

  • generator.throw(exec_type[,exc_value[,traceback]])

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

  • generator.close()

致使生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器没有处理这个异常,或者抛出了StopIteration异常(通常是指运行到结尾),调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。生成器抛出的其他异常会向上冒泡,传给调用方。

示例:调用

class DemoException(Exception):
    """自定义异常"""

def demo_exc_handling():
    print("-> oroutine started")

    while True:
        try :
            x = yield
        except DemoException:
            print('*** DemoException handled. Continuing...')
        else :
            print('-> coroutine received: {!r}'.format(x))

    raise RuntimeError('永远不执行')

exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
exc_coro.close()
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))

如果传入 DemoException,则协程会正常处理,并继续运行。

exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
# 将 DemoException 传入
exc_coro.throw(DemoException)
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))

如果传入的异常没有处理,协程会立即停止,变成'GEN_CLOSED'状态。

exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
# 将 DemoException 传入
exc_coro.throw(ZeroDivisionError)
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))

如果协程必须做一些清理工作,则可以在协程体中放入 try/finally 代码块。

def demo_finally():
    print('-> coroutine started')
    try:
        while True:
            try:
                x = yield
            except DemoException:
                print('*** DemoException handled. Continuing...')
            else:
                print('-> coroutine received: {!r}'.format(x))
    finally:
        print("->coroutine ending...")

demo_coro = demo_finally();
next(demo_coro)
demo_coro.send(11)
demo_coro.send(ZeroDivisionError)

让协程返回值

协程可以在执行时不产出值,而是在最后返回一个值(通常是累计值)。

例子:一次返回平均值

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)

coro_avg = averager();
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(20)
coro_avg.send(None)

#输出:
Traceback (most recent call last):
  File "C:/Users/42072/PycharmProjects/day01/ready04/cor.py", line 109, in <module>
    coro_avg.send(None)
StopIteration: Result(count=3, average=20.0)

发送None会终止循环,导致协程结束,返回结果。一如既往,生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回的值。

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

print(result)

yield from结构会在内部自动捕获StopIteration异常。对yield from结构来说,解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from表达式的值。

yield from 句法

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

用来简化for循环中的yield表达式

def gen():
    for c in 'AB':
        yield c

    for i in range(1,3):
        yield i

print(list(gen()))

# 可以改写为:  

def gen():
    yield from 'AB'
    yield from range(1,3)
    
print(list(gen()))

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

把职责委托给子生成器

yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。通过这个结构,协程可以把功能委托给子生成器。

主要术语:

  1. 委派生成器:
    包含 yield from <iterable> 表达式的生成器函数。

  2. 子生成器:
    从 yield from 表达式中 <iterable>部分获取的生成器。

  3. 调用方
    调用委派生成器的客户端代码。

graph LR
调用方-->委派生成器
委派生成器-->子生成器

示例:
计算7年级学生的体重和身高的平均值:

from collections import namedtuple

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

# 子生成器
def averager():
    total = 0.0
    count = 0
    average = None

    while True:
        # main 方法中发送的各种值,会绑定到term变量上
        term = yield
        
        # 子生成器终止的条件
        if term is None:
            break

        total += term
        count += 1
        average = total / count
    
    # 返回值会成为grouper中 yield from表达式的值
    return Result(count,average)

# 委派生成器
def grouper(results,key):
    while True:
        # 每次迭代都会生成一个averager实例。每个生成器都是本协程(grouper)使用的生成器对象。
        results[key] = yield from averager()

# 客户端代码
def main(data):
    results = {}
    for key,values in data.items():
        # results 用来存储结果
        group = grouper(results, key)
        # 预激活协程
        next(group)
        for value in values:
            # 发送的每个值都会经由grouper的yield from处理,通过管道传给averager实例。同时,当前的grouper实例,会在yield from 处暂停。
            group.send(value)
        # 把None值传入grouper,导致当前的averager实例终止,并让grouper继续运行,再创建一个aveager实例,处理下一组值。
        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]
}

main(data)

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

委派生成器在yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,此时委派生成器会恢复运行。

注意:

  • 如果子生成器不终止,委派生成器会在yield from处永远暂停。
  • 因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用yield from调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个子生成器,以此类推。最终,这个链条要以一个只使用yield表达式的简单生成器结束;不过,也能以任何可迭代的对象结束。
  • 任何yield from链条都必须由客户驱动,在最外层委派生成器上调用next(...)函数或.send(...)方法。

yield from的意义

  1. 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
  2. 使用send()方法发给委派生成器的值都直接传给子生成器。如果发送的值是None,那么会调用子生成器的next()方法。如果发送的值不是None,那么会调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
  3. 生成器退出时,生成器(或子生成器)中的return expr表达式会触发StopIteration(expr)异常抛出。
  4. yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。
  5. 传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法。如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡,传给委派生成器。
  6. 如果把GeneratorExit异常传入委派生成器,或者在委派生成器上调用close()方法,那么在子生成器上调用close()方法,如果它有的话。如果调用close()方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出GeneratorExit异常。

协程能自然地表述很多算法,例如仿真、游戏、异步I/O,以及其他事件驱动型编程形式或协作式多任务。

案例:出租车运营仿真

import collections
# time 事件发生时间  proc:出租车编号  action:活动描述
Event = collections.namedtuple('Event','time proc action')

# 每辆出租车调用一次该函数,用于创建一个生成器对象,用来表示各辆出租车的运营过程。
# ident是出租车编号
# trips 是出租车回家之前的形成数量
def taxi_process(ident,trips, start_time = 0):
    """每次改变状态时创建事件,把控制权让给仿真器"""

    # 离开停车场事件,执行到此,会暂停。当需要重新激活这个进程时,主循环会使用send方法发送当前的仿真事件赋值给time
    time = yield Event(start_time,ident,'leave garage')

    #每次行程都会执行一遍此处的代码块
    for i in range(trips):
        # 产生一个Event实例,表示拉到乘客了。协程在这里会暂停,需要激活时,主循环会使用send方法发送当前时间。
        time = yield Event(time,ident,'pick up passenger')
        time = yield Event(time,ident,'drop off passenger')

    # 指定行程数量完成后,产生回家事件。此处,协程最后一次暂停。
    yield Event(time,ident,'going home')


taxi = taxi_process(ident=12,trips=2,start_time=0)
next(taxi)
print(taxi.send( 7))
print(taxi.send(10))
print(taxi.send(15))
print(taxi.send(25))
print(taxi.send(35))

# 输出:
Event(time=7, proc=12, action='pick up passenger')
Event(time=10, proc=12, action='drop off passenger')
Event(time=15, proc=12, action='pick up passenger')
Event(time=25, proc=12, action='drop off passenger')
Event(time=35, proc=12, action='going home')

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容