美文网首页
异步-协程-yield in Python, 2022-06-1

异步-协程-yield in Python, 2022-06-1

作者: Mc杰夫 | 来源:发表于2022-06-13 22:53 被阅读0次

    (2022.06.13 Mon)
    协程往往和线程做对比。协程也是并发的一种,协程与线程不同之处在于:

    • 线程的调度由CPU执行,协程的调用由开发者写的函数执行
    • 协程更轻量级
    • 协程运行与同一个线程中

    在Python中协程可由生成器实现,某种程度上可以作为流程控制工具的yield也可以实现协作式多任务,似乎是为协程而设计。

    生成器的基本行为

    首先定义一个生成器函数。

    def simple_coroutine(): # 1
        print('-> coroutine started')
        x = yield # 2
        print('-> coroutine received:', x)
    

    simple_coroutine实例,得到一个生成器对象

    >> a = simple_coroutine()
    >> a
    <generator object simple_coroutine at 0x7ffe3131cdd0>
    

    首先回忆生成器的使用方法。yield关键字右边的部分如果为空,则该yield关键字只接受用户传入的值,不返回值给用户,或返回Noneyield右边的变量是每次通过next方法调用生成器时返回给用户的变量。yield左边赋值的变量,即x = yield,表示用户在通过send方法调用生成器时传入的值将会保存在变量x中。

    通过next函数调用生成器,并用send方法传入数值到生成器中。

    >> next(a)
    -> coroutine started
    >> a.send(998)
    -> coroutine received: 998
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration
    

    (2022.06.14 Tues)
    上面的例子yield的等号左边被赋值,也就是caller可以传递值进生成器,另有一种不赋值,不能传值进生成器的情况。比如用yield产生num到100间所有的奇数的方式。

    def generator_odd(num):
        if num > 100:
            num = num % 100
        for i in range(num, 101):
            if i%2 == 1:
                yield i
    

    调用

    >> geno = generator_odd(83)
    >> next(geno)
    83
    >> type(geno)
    <class 'generator'>
    >> list(geno)
    [85, 87, 89, 91, 93, 95, 97, 99]
    

    协程的状态

    协程分为四种状态,通过inspect.getgeneratorstate可查询生成器处在协程的哪种状态。四种状态分别是

    • GEN_CREATED:生成器等待开始执行,创建之后
    • GEN_RUNNING:生成器解释执行中
    • GEN_SUSPENDED:在表达式处暂停
    • GEN_CLOSE:执行结束
    >> from inspect import getgeneratorstate
    >> getgeneratorstate(a)
    'GEN_CLOSED'
    

    协程的预激(prime)

    在生成器的第一次调用时,如果使用send方法传入一个非None到生成器中,则会返回TypeError错误。

    >> a = simple_coroutine()
    >> a.send(10)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: can't send non-None value to a just-started coroutine
    

    通常,在一个生成器第一步使用时,调用next方法,用以预激协程,也就是让协程向前执行到第一个yield表达式,准备好作为活跃的协程使用。

    预激的方法有两种:

    • 调用next方法
    • 调用send方法传入None

    为简化预激协程的过程,可使用装饰器对生成器函数进行装饰。

    >> def coroutine(func):
           @wraps(func)
           def primer(*args, **kwargs):
               gen = func(*args, **kwargs) # 调用被装饰的函数,获取生成器对象
               next(gen) # 预激生成器
               return gen # 返回生成器
           return primer
    

    用该协程函数,装饰计算平均值的生成器。

    @coroutine
    def average():
        total, cntr, average = 0, 0, None
        while True:
            tmp = yield average
            total += tmp
            cntr += 1
            average = total/cntr
    

    调用average函数创建一个生成器对象,注意因为该函数已经被coroutine装饰,已经完成了预激,在生成器对象创建之后可直接向其中传入值,而不必预激

    >> b = average()
    >> type(b)
    <class 'generator'>
    >> inspect.getgeneratorstate(b)
    'GEN_SUSPENDED'
    >> b.send(1000)
    1000.0
    

    注意到,在创建生成器对象后,查看生成器的状态,因为装饰器中已经完成了预激,所以状态为GEN_SUSPENDED。作为对比,查看一个为被装饰的生成器返回的状态是'GEN_CREATED'。

    def noprime():
        c = 1
        tmp = yield c
        c += 1
    
    >> np = noprime()
    >> inspect.getgeneratorstate(np)
    'GEN_CREATED'
    

    协程的执行顺序

    有这样一个生成器函数

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

    运行结果如下

    >> d = simple_coro2(5)
    >> next(d) # 1
    -> Started: a = 5 # 2
    5 # 3
    >> d.send(99) 
    -> Received: b = 99 # 4
    104 # 5 
    >> d.send(25)
    -> Received: c = 25 # 6 
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration
    

    # 1:预激,运行代码到第一个yield处,于是有了# 2和# 3
    # 2:打印第一个yield之前的print指令
    # 3:返回yield a中的a
    # 4和# 5:从第一个b = yield a的等号左边开始运行,到下一个c = yield a + b的等号右边,于是有了# 5的104,因a+b=104
    # 6:运行c = yield a + b的等号左边,接受了来自用户的send赋值,并运行到生成器结尾,以StopIteration退出

    从上面的过程可以看到,对于能接受用户赋值的yield表达式,每次运行到yield表达式的等号右侧,下次调用nextsendyield的表达式左侧开始运行,顺序如图所示

    Coroutine running order

    终止协程和异常处理

    继续考虑求平均值的生成器

    @coroutine
    def average():
        total, cntr, average = 0, 0, None
        while True:
            tmp = yield average
            total += tmp
            cntr += 1
            average = total/cntr
    

    创建生成器,并传递不符合要求的类型

    >> f = average()
    >> getgeneratorstate(f)
    'GEN_SUSPENDED'
    >> f.send('s')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 6, in average
    TypeError: unsupported operand type(s) for +=: 'int' and 'str'
    >> f.send(5)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration
    

    这个案例给出了一种终止协程的方式:发送哨符值,比如None。使用生成器对象的close方法可以关闭协程。

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

    协程的异常处理,可参考下面例子

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

    创建生成器对象,并用throw方法抛出异常

    >> de = demo_exc_handing()
    >> de.send(None)
    -> coroutine started
    >> de.throw(DemoException) # 1
    ** DemoException handled. Continuing...
    >> de.send(2)  # 2
    -> coroutine received: 2
    

    # 1:抛出DemoException异常
    # 2:上一步抛出异常后,协程并没有终止

    (2022.06.14 Tues)

    让协程返回值

    协程返回值最简单的方法是当检测到caller通过send传入的值为特定值,如None时,跳出生成器的循环条件,并返回指定的值。考虑计算平均值的例子。

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

    调用得到

    >> f = average()
    >> f.send(None)
    >> f.send(1)
    >> f.send(3)
    >> f.send(10)
    >> f.send(None)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration: (3, 4.666666666666667)
    

    注意到生成器函数的返回结果,即return (count, average)部分,是StopIteration的属性,所以可以在调用时catch这个异常,并调用该属性。

    >> f = average()
    >> f.send(None) # prime coroutine
    >> f.send(10)
    >> f.send(20)
    >> try:
    ...     f.send(None)
    ... except StopIteration as s:
    ...     tmp = s.value
    ... 
    >> tmp
    (2, 15.0)
    

    yield from

    yield from语法出现在Python 3.3之后,其后接应的对象是可迭代对象,迭代器,和生成器,用于简化for循环中的yield表达式。仅使用yield指令的情况下有如下表达:

    def gen():
        for c in 'AB':
            yield c
        for i in range(1, 3):
            yield i
    

    调用

    >> list(gen())
    ['A', 'B', 1, 2]
    

    如果使用yield from表达,gen方法可以简化为下面这种形式,且调用结果完全相同。

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

    yield from加可迭代对象,可以把可迭代对象中的每个元素一一yield出来。

    yield from后面接上生成器,就得到了生成器的嵌套。尽管生成器的嵌套并非一定要用yield from方法,但这个方法可以避免不必要的麻烦。

    研究生成器的嵌套前首先明确几个概念:

    1. 调用方:调⽤委派⽣成器的客户端(调⽤⽅)代码
    2. 委托生成器:包含yield from表达式的⽣成器函数
    3. 子生成器:yield from后⾯加的⽣成器函数

    下面用求平均的案例来解释上面几个概念

    # 子生成器
    def average():
        total, cntr, average = 0, 0, None
        while True:
            tmp = yield average
            total += tmp
            cntr += 1
            average = total/cntr
    # 委托生成器
    def proxy_gen():
        while True:
            yield from average()
    # 调用方
    def main():
        calc_average = proxy_gen()
        next(calc_average) # 预激生成器
        print(calc_average.send(10))  # 10.0
        print(calc_average.send(20))  # 15.0
        print(calc_average.send(30))  # 20.0
    

    委托生成器的作用是在调用方和子生成器之间建立一个双向通道。

    下面查看一下当出现异常时,对委托生成器做修改后如何处理异常。

    def average_gen():
        total =0
        count =0
        average =0
        while True:
            new_num = yield average
            if new_num is None:
                break
            count += 1
            total += new_num
            average = total/count
        return total, count, average
    def proxy_gen():
        while True:
        # 只有⼦⽣成器要结束(return)了,yield from左边的变量才会被赋值,后⾯的代码才会执⾏。
            total, count, average = yield from average_gen()
            print("计算完毕\n 总共传⼊{} 个数值,总和:{},平均数:{}".format(count, total, average))
    def main():
        calc_average = proxy_gen()
        next(calc_average)
        print(calc_average.send(10))  #
        print(calc_average.send(20))  #
        print(calc_average.send(30))  #
        calc_average.send(None)
    

    返回结果

    >> main()
    10.0
    15.0
    20.0
    计算完毕!!
    总共传⼊3 个数值,总和:60,平均数:20.0
    

    如果避开委托生成器,直接对子生成器发送值和None,则最后返回StopIteration和对应的total等值。使用了委托生成器则能优雅的处理异常并返回打印结果。

    >>> a = average_gen()
    >>> a.send(None)
    0
    >>> a.send(10)
    10.0
    >>> a.send(20)
    15.0
    >>> a.send(40)
    23.333333333333332
    >>> a.send(None)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    StopIteration: (70, 3, 23.333333333333332)
    

    当然委托生成器不只是处理异常,StopIteration异常处理完全可以自己手写完成。委托生成器做了更多。

    下面的伪代码,等效于委派生成器中的RESULT = yield from EXPR语句。以下代码过于复杂,可以简单的理解为yield from帮忙做了很多的异常处理。

    _i = iter(EXPR) 
    try:
        _y = next(_i) 
    except StopIteration as _e:
        _r = _e.value 
    else:
        while 1: 
            try:
                _s = yield _y 
            except GeneratorExit as _e: 
                try:
                    _m = _i.close
                except AttributeError:
                    pass
                else:
                    _m()
                raise _e
            except BaseException as _e: 
                _x = sys.exc_info()
                try:
                    _m = _i.throw
                except AttributeError:
                    raise _e
                else: 
                    try:
                        _y = _m(*_x)
                    except StopIteration as _e:
                        _r = _e.value
                        break
            else: 
                try: 
                    if _s is None: 
                        _y = next(_i)
                    else:
                        _y = _i.send(_s)
                except StopIteration as _e: 
                    _r = _e.value
                    break
    RESULT = _r 
    

    以上代码的说明如下:

    1. 迭代器(即可指⼦⽣成器)产⽣的值直接返还给调⽤者
    2. 任何使⽤send()⽅法发给委派⽣产器(即外部⽣产器)的值被直接传递给迭代器。如果send值是None,则调⽤迭代器next()⽅法;如果不为None,则调⽤迭代器的send()⽅法。如果对迭代器的调⽤产⽣StopIteration异常,委派⽣产器恢复继续执⾏yield from后⾯的语句;若迭代器产⽣其他任何异常,则都传递给委派⽣产器。
    3. ⼦⽣成器可能只是⼀个迭代器,并不是⼀个作为协程的⽣成器,所以它不⽀持throw()close()⽅法,即可能会产⽣AttributeError异常。
    4. 除了GeneratorExit异常外的其他抛给委派⽣产器的异常,将会被传递到迭代器的throw()⽅法。如果迭代器throw()调⽤产⽣了StopIteration异常,委派⽣产器恢复并继续执⾏,其他异常则传递给委派⽣产器。
    1. 如果GeneratorExit异常被抛给委派⽣产器,或者委派⽣产器的close()⽅法被调⽤,如果迭代器有close()的话也将被调⽤。如果close()调⽤产⽣异常,异常将传递给委派⽣产器。否则,委派⽣产器将抛出GeneratorExit异常。
    2. 当迭代器结束并抛出异常时,yield from表达式的值是其StopIteration异常中的第⼀个参数。
    3. ⼀个⽣成器中的return expr语句将会从⽣成器退出并抛出StopIteration(expr)异常。

    Reference

    1 流畅的Python,Luciano R. 著,安道等译,中国工信出版社,人民邮电出版社
    2 百度文库-Python并发编程之深入理解yieldfrom语法(八)

    相关文章

      网友评论

          本文标题:异步-协程-yield in Python, 2022-06-1

          本文链接:https://www.haomeiwen.com/subject/wyoomrtx.html