资讯专栏INFORMATION COLUMN

Python中的协程

shinezejian / 2044人阅读

摘要:协程的基本行为协程包含四种状态等待开始执行。协程中重要的两个方法调用方把数据提供给协程。注意使用调用协程时会自动预激,因此与装饰器不兼容标准库中的装饰器不会预激协程,因此能兼容句法。因此,终止协程的本质在于向协程发送其无法处理的异常。

导语:本文章记录了本人在学习Python基础之控制流程篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。

本文重点:

1、掌握协程的概念与行为;
2、掌握协程中的预激,终止和异常处理;
3、深入理解yield from的本质作用。
一、协程介绍 1、协程概述

协程:指的是与调用方协作,产出由调用方提供的值。
语法结构:协程是定义体中包含yield关键字的函数,一般使用生成器函数定义。
意义:协程中的yield关键字是一种控制流程工具。即不管数据如何流动,协程都会把控制权让步给中心调度程序,从而激活其他的协程实现协作式多任务。

2、协程的基本行为

协程包含四种状态:

GEN_CREATED:等待开始执行。

GEN_RUNNING:解释器正在执行。

GEN_SUSPENDED:在yield表达式处暂停。

GEN_CLOSED:执行结束。

可使用inspect.getgeneratorstate(...)查询协程所处的状态。

协程中重要的两个方法:

.send(datum):调用方把数据提供给协程。

next(coroutine):预激协程。

协程返回值:自Python3.3实现PEP 380以来对生成器函数做了两处改动,一处是生成器可以返回值。

3、实例1:协程初级使用——计算平均值

下面将利用协程计算用户传入若干数值的平均值。

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

process=average()
next(process)#预激协程
process.send(5)#输出5
process.send(10)#输出7.5
process.send(15) #输出10.0

小结:协程执行首先需要预激,使之准备好然后让步控制权。具体地说,协程在yield关键字所在的位置暂停执行。在term=yield average这个 赋值语句中,右边的代码会在赋值之前执行。 在暂停结束后,从先前阻塞的那行代码开始,将yield 表达式的值赋给左边的变量。

实例2:令协程返回值

from collections import namedtuple

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

def average():
    total = 0.0
    number = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        number += 1
        average=total/number
    return Result(average,number) 

分析:当协程终止时,可以在return表达式中返回值。并且return表达式通过把值绑定到StopIteration的value属性上传给调用方返回值。事实上这也符合生成器的常规行为——耗尽时抛出StopIteration异常。

二、协程的预激、终止与异常处理 1、预激协程

协程在使用前须预激,让协程向前执行到第一个yield表达式,准备好作为活跃的协程使用。
预激的本质方法:

next(coroutine):常见的标准方法。

同时首次发送coroutine.send(None)也可以调用next(coroutine),实现相同功能,但缺乏可读性。

基于本质方法,我们衍生出自定义预激协程的装饰器的方法,避免忘记预激协程。
coroutine:预激协程的装饰器

from functools import wraps
def coroutine(func):
    @wraps(func)#把func相关属性复制过来
    def manage(*args,**kwargs):
        gen=func(*args,**kwargs)#获取生成器对象
        next(gen)#预激协程
        return gen#返回协程
    return manage

只需将@coroutine语法糖加在生成器函数上,就可以通过构造生成器对象获取活跃的协程。
注意:

使用yield from调用协程时会自动预激,因此与@coroutine装饰器不兼容;

Python3.4标准库中的asyncio.coroutine装饰器不会预激协程,因此能兼容yield from句法。

2、终止协程

协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方。
因此,终止协程的本质在于向协程发送其无法处理的异常。下面介绍三种方法终止协程:

发送哨符值。常用None和Ellipsis,甚至StopIteration类也可以发送。

generator.throw(exc_type[,exc_value[,traceback]])
令生成器在暂停的yield表达式处抛出指定的异常。若生成器处理了此异常,则生成器向前执行到下一个yield表达式,而产出的值会成为调用generator.throw得到的返回值。否则,异常会向上冒泡,传到调用方的上下文中。

generator.close()
令生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器不处理此异常,或者跑出来StopIteration,调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。

后两种方法是自Python2.5开始显式发送异常的两个方法,建议使用后两种方法来终止协程。

3、处理异常

在使用协程的过程中会产生一些需要处理的异常,此时可利用try/except处理。如果不管协程如何结束都要做一些清理工作,请使用try/finally处理。

实例1:使用try/finally在协程终止时执行操作

class DemoException(Exception):
"""为这次演示定义的异常类型。 """
    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")
三、yield from结构 1、yield from结构介绍

作用介绍:
本质作用是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样两者可以直接发送和产出值,还可以直接传入异常。
替代产出值的嵌套for循环。

执行机制:
(1)在生成器gen中使用yield from subgen()时,subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。与此同时,gen会阻塞,等待subgen终止。
(2)yield from结构会在内部自动捕获StopIteration异常,还会把对应的value属性值变成yield from表达式的值。

2、yield from的应用

实例1:对yield from架构双向通道本质的深入理解
下面我们结合实例深入理解yield from结构。假设我们需要利用yield from分别计算一个班级男女生身高和体重的平均值,并予以输出。采用“外部调用方+委派生成器+子生成器”的结构进行设计,结构示意图如下:

实例代码:

from collections import namedtuple
Result=namedtuple("Result","average number")

def subaverager():#子生成器经委派生成器处理外部数据,并将值返回给委派生成器。
    total = 0.0
    number = 0
    average = None
    while True:
        term = yield
        if term is None:#外部调用方控制子生成器终止的关键语句。
            break
        total += term
        number += 1
        average=total/number
    return Result(average,number)

def averager(results,key):#委托生成器架构双向通道。
    while True:#避免StopIteration。当得到子生成器的返回值时,程序会执行到下一个yield。
        results[key]=yield from subaverager()

def main(grouper):
    results={}
    for key,group in grouper.items():
        term = averager(results,key)#构建生成器对象。
        next(term)
        for value in group:
            term.send(value)
        term.send(None)#外部调用方控制子生成器终止的语句。
        print(results)
    result(results)

def result(results):#格式化输出协程返回的处理数据。
    for key,value in results.items():
        gender,unit=key.split(";")
        print("{} {} averaging {:.2f} {}.".format(
            value.number,gender,value.average,unit))

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)

思路扩展:上例展示的结构中仅有一个委派生成器和一个子生成器。事实上,这种调用关系可以扩展到更多的委托生成器上。即把多个委派生成器连接到一起。一个委派生成器调用另一个子生成器,这个子生成器本身也是委派生成器。这种链式结构最终以一个只使用yield的简单生成器结束,或者任何的可迭代对象结束。

实例2:替代产出值的嵌套for循环

def gen():
for c in "AB":
 yield c
 for i in range(1, 3):
 yield i
print(list(gen()))#输出["A", "B", 1, 2]

可以简化成:

def gen():
yield from "AB"
yield from range(1, 3)
print(list(gen()))#输出["A", "B", 1, 2]
3、PEP380中总结的yield from的六点行为

子生成器产出的值都直接传给委派生成器的调用方。

使用send()方法发给委派生成器的值都直接传给子生成器。如果发送None会调用生成器的__next__()方法。如果发送的不是None,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。

生成器退出时,生成器中的return表达式会触发StopIteration(expr)异常抛出。

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

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

如果把 GeneratorExit 异常传入委派生成器, 或者在委派生成器上调用 close() 方法, 那么在子生成器上调用 close() 方法, 如果它有的话。 如果调用 close() 方法导致异常抛出, 那么异常会向上冒泡, 传给委派生成器; 否则, 委派生成器抛出GeneratorExit 异常。

四、协程与生成器异同

生成器用于生成供迭代的数据。

协程是数据的消费者,能完成协作式多任务活动。

协程与迭代无关。尽管在协程中会使用 yield 产出值, 但这与迭代无关。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/44636.html

相关文章

  • Python中的并发处理之使用asyncio包

    摘要:并发用于制定方案,用来解决可能但未必并行的问题。在协程中使用需要注意两点使用链接的多个协程最终必须由不是协程的调用方驱动,调用方显式或隐式在最外层委派生成器上调用函数或方法。对象可以取消取消后会在协程当前暂停的处抛出异常。 导语:本文章记录了本人在学习Python基础之控制流程篇的重点知识及个人心得,打算入门Python的朋友们可以来一起学习并交流。 本文重点: 1、了解asyncio...

    tuniutech 评论0 收藏0
  • Tornado 4.3文档翻译: 用户指南-协程

    摘要:译者说于年月日发布,该版本正式支持的关键字,并且用旧版本编译同样可以使用这两个关键字,这无疑是一种进步。其次,这是最后一个支持和的版本了,在后续的版本了会移除对它们的兼容。 译者说 Tornado 4.3于2015年11月6日发布,该版本正式支持Python3.5的async/await关键字,并且用旧版本CPython编译Tornado同样可以使用这两个关键字,这无疑是一种进步。其次...

    SimonMa 评论0 收藏0
  • 协程原理】 - 为什么greenlet的状态无法被保存

    摘要:特别是最火的协程框架也无法保存状态,让人非常惋惜。但是因为栈的本身无法持久化,所以也就无法持久化。其难度在于,假设整个要持久化的调用栈全部都是内的,比如纯的。采取的是暴力地把整个栈区域拷贝到上的方式来保存其状态。 python主流的协程实现有五种: cPython的generator cPython的greenlet cPython的fibers stackless python ...

    verano 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<