【python】协程:协程与生成器的对比、激活协程、终止协程和异常处理

基于生成器的协程

生成器可以作为协程(coroutine)使用,称为 "基于生成器的协程"。
协程和生成器类似,都是定义体中包含 yield 关键字的函数。但它们也有本质区别:

  • 生成器用于生成 供迭代的数据,next()方法只允许调用方从生成器中获取数据;
  • 而协程与迭代无关,协程是数据的消费者,调用方会把数据推送给协程。

【知识拓展】:
2001年,Python 2.2 通过了 PEP 255 -- Simple Generators ,引入了yield 关键字实现了生成器函数,yield 包含 产出 和 让步两个含义: 生成器中yield x这行代码会 产出 一个值,提供给 next(...) 的调用方; 此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用 next(...);

2005年,Python 2.5 通过了 PEP 342 - "Coroutines via Enhanced Generators",给生成器增加了.send()、.throw()和.close()方法,第一次实现了基于生成器的协程函数(generator-based coroutines)


协程的简单使用
  • (coroutine function):协程函数;
  • (coroutine object):协程对象;

生成器的调用方可以使用.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) # ③

OUTPUT:

->coroutine started
Traceback (most recent call last):
-> coroutine received: 42
  File "C:/Users/admin/Desktop/untitled1/6-30/coroutine.6-30.py", line 129, in <module>
    my_coro.send(42) # ③
StopIteration

【解释一下】:

  • ① yield在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是None——这个值是隐式指定的,因为yield关键字右边没有表达式。

  • ② 首先要调用next(...)函数,因为生成器还没启动,没在yield语句处暂停,所以一开始无法发送数据。

  • ③ 调用这个方法后,协程定义体中的yield表达式会计算出42;现在,协程会恢复,一直运行到下一个yield表达式,或者终止。


协程与生成器的对比
  • (生成器)generators are data producers:

生成器用于生成供迭代的数据,next()方法只允许调用方从生成器中获取数据;

  • (协程)coroutines are data consumers:

协程是数据的消费者,调用方会把数据推送给协程。send()方法允许调用方和协程之间双向交换数据。

【注意】: 协程也可以产出值,但这与迭代无关;


协程的状态
  • 'GEN_CREATED': 等待开始执行;

  • 'GEN_RUNNING': 正在被解释器执行。只有在多线程应用中才能看到这个状态;

  • 'GEN_SUSPENDED': 在yield表达式处暂停;

  • 'GEN_CLOSED': 执行结束;

【注释】:可以使用 inspect.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(27)
print(getgeneratorstate(my_coro))

Output:

GEN_CREATED
Traceback (most recent call last):
->coroutine started
GEN_SUSPENDED
  File "C:/Users/admin......(嘻嘻,保密)......py", line 144, in <module>
GEN_RUNNING
    my_coro.send(27)
-> coroutine received: 27
StopIteration

【注意点】

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

  • 如果给未激活的协程对象发送None以外的值,会引发错误 (避免小错误哦)。


激活协程的方式
  • next(...)方法;
  • 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 = 0.0

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

cor_avg = averager()
next(cor_avg)
cor_avg.send(20)
cor_avg.send(5)
cor_avg.send(27)
----
输出:
20.0
12.5
17.333333333333332

【知识拓展】:关于wraps(写的很详细):
https://blog.csdn.net/hqzxsc2006/article/details/50337865


Python装饰器

在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响,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__)

Output:
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))

Output:

GEN_SUSPENDED
10.0
20.0

【解释】:如果使用yield from 语法,会自动预激。

asyncio.coroutine装饰器不会预激活协程,因此能兼容yield from 句法。


终止协程和异常处理

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

  • 由于第三行发送的不是数据,导致协程内部抛出异常;
  • 由于协程内部没有处理异常,协程会终止。如果试图重新激活协程,会抛出StopIteration异常;
异常处理的两种方式
  • generator.throw(exec_type[,exc_value[,traceback]])
  • 会使生成器在暂停的yield表达式处抛出指定的异常;

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

  • 如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中;


  • generator.close()
  • 致使生成器在暂停的yield表达式处抛出GeneratorExit异常(正常终止协程)。

  • 如果生成器没有处理这个异常,或者抛出了StopIteration异常(通常是指运行到结尾),调用方不会报错;

  • 如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。生成器抛出的其他异常会向上冒泡,传给调用方。

【注意1】如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常;
生成器抛出的其他异常会向上冒泡,传递给调用方;
【注意2】但是,如果传入协程的异常没有处理,协程会终止,即状态变成 'GEN_CLOSED':

呈上一份代码:

class DemoException(Exception):
    pass

def demo_exc_handing():
    print('==>coroutine strated')

    while True:
        try:
            x = yield       # 产生异常!
        except DemoException:  # 特别处理 DemoException 异常
            print('==> DemoException handled.Continuing...')
        else:         # 如果没有异常,那么显示接收到的值
            print('==>coroutine received:{!r}'.format(x))
        finally:
            print('==>Ending...')
exc_coro = demo_exc_handing()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)

print(inspect.getgeneratorstate(exc_coro))

输出:

==>Ending...
==>coroutine strated
==>coroutine received:11
==>Ending...
==>coroutine received:22
==>Ending...
GEN_SUSPENDED
基于jupyter-notebook

【解释】如果把DemoException异常传入demo_finally协程,它会处理,然后继续运行

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)

让协程return值

Python 3.3以后,允许在协程中有return expr表达式,如果执行到该语句,则协程运行结束。同时会抛出StopIteration异常,而返回值就在该异常对象的value属性上。
【重点】如果协程中有return语句,则返回return后面的表达式的值,否则返回None
有些协程在被激活后,每次驱动(drive)协程时,不会产出值,而是在最后(协程正常终止时)返回一个值(通常是某种累加值)

还是上述讲过的例子,在这里会返回平均值:

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)

Output:

Traceback (most recent call last):
  File "C:/Users/...(保密,嘻嘻)...py", line 25, in <module>
    coro_avg.send(None)
StopIteration: Result(count=3, average=20.0)

生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回的值.....
【yield from结构】:在内部自动捕获StopIteration异常。对yield from结构来说,解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from表达式的值。

使用协程的优点
  • 协程最大的优势就是协程极高的执行效率。子程序切换由程序自身控制,因此,没有线程切换的开销;
  • 不需要多线程的锁机制,阻塞自动切换;
  • 程即是生成器对象的yield和send方法的配合使用;

【关于python中的 yield from 句法】:https://www.jianshu.com/p/6c6761a407c5


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 目录:一、基于生成器的协程二、协程状态三、协程预激装饰器四、终止协程和异常处理五、协程返回值六、yield fro...
    Recalcitrant阅读 375评论 0 0
  • 从语法上来看,协程和生成器类似,都是定义体中包含yield关键字的函数。yield在协程中的用法: 在协程中yie...
    忘了呼吸的那只猫阅读 330评论 0 2
  • 协程 to yield 含义:产出和让步。 yield item这行代码会产出一个值,提供给next(...)的调...
    风果常识阅读 925评论 0 3
  • 从语法上来看,协程和生成器类似,都是定义体中包含yield关键字的函数。yield在协程中的用法:在协程中yiel...
    JokerW阅读 1,801评论 0 0
  • 001 安全感和铁饭碗不是稳定地工作带来的,而是自己的本事,如果碰见政策洗牌,时代换血,要有对自我负责的能力。 0...
    金一鹤阅读 167评论 0 0