欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

《流畅的Python》10-协程初步

程序员文章站 2022-05-08 11:30:08
...

协程是放在生成器,迭代器后面讲的,这也是生成器的最终的归宿,可以理解为高阶的特性。如果生成器仅仅是当作语法糖,那么它可以被很容易的被其他形式替代而不会被重视。

同时,作者指出,协程作为一种鲜为人知,资料匮乏的特性,看起来并不是很有用,常常被忽视。实际上关于Python的一般广为人知的特性已经介绍完了,不过事情正变得更有趣。

前面介绍协程,然后介绍新的句法,用yield from来实现一个标准漂亮的协程。

协程的概念

从句法上来看,协程里有一个yield关键字,意思是让步;产出。协程看起来和生成器有点类似,最大的区别在于,它不但能读出数据,还能往里面推送数据。从根本上把 yield 视作控制流程的方式,这样就好理解协程了。

生成器作为协程时的行为和状态

这里要提出,强调的是生成器进化为协程后的行为。用 yield关键字实现的生成器可以视作一个协程。

来一个简单的样例和图片来解释。

计算平均值,并打印
>>> def average():
    count=0.0
    total=0
    average=0.0
    term=yield
    count+=term
    total+=1
    return count/total

  >>> x=average()
  >>> next(x)
  >>> x.send(1)
  1.0
  >>> x.send(2)
  1.5
  >>> x.send(3)
  2.0
  >>> x.send(None)
  Traceback (most recent call last):
    File "<pyshell#31>", line 1, in <module>
      x.send(None)
    File "<pyshell#20>", line 7, in average
      count+=term
  TypeError: unsupported operand type(s) for +=: 'float' and 'NoneType'
  >>>
  • 创建后需要调用next(gen)来预激协程,效果等同于x.send(None)
  • 协程结束后抛出StopIterator异常
  • 程序进行到yield暂停,.send()方法赋值给=左边,与右边无关。

如下图过程的划分。

《流畅的Python》10-协程初步

装饰器自动预激协程

使用一个装饰器,就不用每次开头都调用.next()

这个装饰器的作用就是调用一次next(),并返回原来的函数(@wraps(func)保证原来函数不会被修改)。

from functools import wraps

def coroutine(func):
    """装饰器:向前执行到第一个`yield`表达式,预激`func`"""

    @wraps(func)
    def primer(*args, **kwargs): ➊
        gen = func(*args, **kwargs) ➋
        next(gen) ➌
        return gen ➍
    return primer

终止协程和异常处理

  • .close()方法即停止协程
  • .throw()方法向协程内传递一个异常。

终止协程的一种方式:发送一个暗哨值,让协程退出。
常见的暗哨值有None,Ellipsis,甚至还有StopIteration

处理异常记住一点:
协程内能被(try/finally)处理的异常都能让协程正确进行,如果没有被处理,异常会向上级冒泡,传到调用方的上下文。

新句法! yield from

第 14 章说过,yield from 可用于简化 for 循环中的 yield 表达式。
例如

>>> def gen():
...
yield from 'AB'
...
yield from range(1, 3)
...
>>> list(gen())
['A', 'B', 1, 2]

但是yield fromr远不止这种用法。

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

首先介绍三个术语:

  • 委派生成器
  • 子生成器
  • 调用方

委派生成器

包含 yield from 表达式的生成器函数。

子生成器

从 yield from 表达式中 部分获取的生成器。

调用方

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

实际上通过yield from结构把调用方和(另一个)子生成器分开。而往往yield from也会单独用一个结构比如函数定义。叫做委派生成器。委派生成器在传值过程中没有中间过程,直接联系两者。

一个有点复杂的例子。

from collections import namedtuple

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


def average():
    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)


def grouper(results, key):
    while(True):
        results[key] = yield from average()


def main():
    results = {}
    for key, values in data.items():
        group = grouper(results, key)
        next(group)
        for value in values:
            group.send(value)
        group.send(None)
    report(results)


def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:.5} averaging  {:.2f}{}'.format(
            result.count, group, result.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()

输出:
9 boys averaging  40.42kg
9 boys averaging  1.39m
10 girls averaging  42.04kg
10 girls averaging  1.43m

yield from 的意义

摘抄

  • 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
  • 使用 send()方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的 __next__() 方法。如
    果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运
    行。任何其他异常都会向上冒泡,传给委派生成器。
  • 生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。
  • yield from 表达式的值是子生成器终止时传给 StopIteration异常的第一个参数。

yield from 结构的另外两个特性与异常和终止有关。

  • 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的 throw() 方法。如果调用 throw() 方法时抛出StopIteration 异常,委派生成器恢复运行。StopIteration 之外的异常会向上冒泡,传给委派生成器。
  • 如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么在子生成器上调用 close() 方法,如果它有的话。如果调用 close() 方法导致异常抛出,那么异常会
    向上冒泡,传给委派生成器;否则,委派生成器抛出
    GeneratorExit 异常。

yield from 的伪代码

摘抄

即下面语句的伪代码。

RESULT = yield from EXPR
_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⓭
❶ EXPR 可以是任何可迭代的对象,因为获取迭代器 _i(这是子生成器)使用的是 iter() 函数。

❷ 预激子生成器;结果保存在 _y 中,作为产出的第一个值。

❸ 如果抛出 StopIteration 异常,获取异常对象的 value 属性,赋值给 _r——这是最简单情况下的返回值(RESULT)。

❹ 运行这个循环时,委派生成器会阻塞,只作为调用方和子生成器之间的通道。

❺ 产出子生成器当前产出的元素;等待调用方发送 _s 中保存的值。这个代码清单中只有这一个 yield 表达式。

❻ 这一部分用于关闭委派生成器和子生成器。因为子生成器可以是任何可迭代的对象,所以可能没有 close 方法。

❼ 这一部分处理调用方通过 .throw(...) 方法传入的异常。同样,子生成器可以是迭代器,从而没有 throw 方法可调用——这种情况会导致委派生成器抛出异常。

❽ 如果子生成器有 throw 方法,调用它并传入调用方发来的异常。子生成器可能会处理传入的异常(然后继续循环);可能抛出StopIteration 异常(从中获取结果,赋值给 _r,循环结束);还可能不处理,而是抛出相同的或不同的异常,向上冒泡,传给委派生成器。

❾ 如果产出值时没有异常......

❿ 尝试让子生成器向前执行......

⓫ 如果调用方最后发送的值是 None,在子生成器上调用 next 函数,否则调用 send 方法。

⓬ 如果子生成器抛出 StopIteration 异常,获取 value 属性的值,赋值给 _r,然后退出循环,让委派生成器恢复运行。

⓭ 返回的结果(RESULT)是 _r,即整个 yield from 表达式的值。