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

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

同时,作者指出,协程作为一种鲜为人知,资料匮乏的特性,看起来并不是很有用,常常被忽视。实际上关于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()方法赋值给=左边,与右边无关。

如下图过程的划分。

这里写图片描述

装饰器自动预激协程

使用一个装饰器,就不用每次开头都调用.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 表达式的值。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值