协程中未处理的异常会向上抛出,传给 next 函数或 send 方法的调用方(即触发协程的对象)。下面示例举例说明如何使用示例中由装饰器定义的 averager 协程。

未处理的异常会导致协程终止

from inspect import getgeneratorstate
from functools import wraps


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

    @wraps(func)
    def primer(*args, **kwargs):  # 把被装饰的生成器函数替换成这里的 primer 函数;调用 primer 函数时,返回预激后的 生成器
        gen = func(*args, **kwargs)  # 调用被装饰的函数,获取生成器对象。
        next(gen)  # 预激生成器。
        return gen  # 返回生成器。

    return primer


@coroutine  # 把装饰器应用到 averager 函数上
def averager():  # 实现求平均值
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total / count


if __name__ == '__main__':
    coro_avg = averager()  # 使用 @coroutine 装饰器装饰的 averager 协程,可以立即开始发送值。
    print(coro_avg.send(10))
    # 10.0
    print(coro_avg.send(30))
    # 20.0
    print(coro_avg.send('spam'))  # 发送的值不是数字,导致协程内部有异常抛出。
    #   File "C:/myFiles/company_project/xbot/mytest/test.py", line 34, in <module>
    #     print(coro_avg.send('spam'))  # 发送的值不是数字,导致协程内部有异常抛出。
    #   File "C:/myFiles/company_project/xbot/mytest/test.py", line 24, in averager
    #     total += term
    # TypeError: unsupported operand type(s) for +=: 'float' and 'str'
    print(coro_avg.send(60))  # 由于在协程内没有处理异常,协程会终止。如果试图重新激活协程,会抛出StopIteration异常。
    # StopIteration                             Traceback (most recent call last)
    # <ipython-input-7-23a7fcf06c51> in <module>
    # ----> 1 coro_avg.send(60)
    # 
    # StopIteration:

tips: 出错的原因是,发送给协程的 ‘spam’ 值不能加到 total 变量上。

上面示例,暗示了终止协程的一种方式:发送某个哨符值,让协程退出。内置的 NoneEllipsis 等常量经常用作哨符值。Ellipsis优点是,数据流中不太常有这个值。我还见过有人把 StopIteration 类(类本身,而不是实例,也不抛出)作为哨符值;也就是说,是像这样使用的:my_coro.send(StopIteration)

Python 2.5 开始,客户代码可以在生成器对象上调用两个方法显式地把异常发给协程。这两个方法是 throwclose

  • generator.throw(exc_type[, exc_value[, traceback]]) 致使生成器在暂停的 yield 表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个 yield 表达式,而产出的值会成为调用 generator.throw 方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上抛,传到调用方的上下
    文中。
  • generator.close() 致使生成器在暂停的 yield 表达式处抛出 GeneratorExit 异常。如果生成器没有处理这个异常,或者抛出了 StopIteration 异常(通常是指运行到结尾),调用方不会报错。如果收到 GeneratorExit 异常,生成器一定不能产出值,否则解释器会抛出RuntimeError 异常。生成器抛出的其他异常会向上抛,传给调用方。

tips:
生成器对象方法的官方文档深藏在 Python 语言参考手册中,参见“6.2.9.1. Generator-iterator methods”(https://docs.python.org/3/reference/expressions.html#generator-iterator-methods)。

下面举例说明如何使用 closethrow 方法控制协程。

# -*- coding: utf-8 -*-
from inspect import getgeneratorstate


class DemoException(Exception):
    """为这次演示定义的异常类型。"""


def demo_exc_handling():
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:  # 特别处理 DemoException 异常。
            print('*** DemoException handled. Continuing...')
        else:  # 如果没有异常,那么显示接收到的值。
            print('-> coroutine received: {!r}'.format(x))

    # 这一行永远不会执行。因为只有未处理的异常才会中止无限循环,而
    # 一旦出现未处理的异常,协程会立即终止
    raise RuntimeError('This line should never run.')


if __name__ == '__main__':
    exc_coro = demo_exc_handling()
    next(exc_coro)

    # -> coroutine started
    exc_coro.send(11)
    # -> coroutine received: 11
    exc_coro.send(22)  # 正常send
    # -> coroutine received: 22

    print(exc_coro.throw(DemoException))  # 把 DemoException 异常传入 demo_exc_handling 不会导致协程中止
    # *** DemoException handled. Continuing...
    print(getgeneratorstate(exc_coro))
    # GEN_SUSPENDED

    exc_coro.throw(ZeroDivisionError)  # 如果无法处理传入的异常,协程会终止
    # Traceback (most recent call last):
    #   File "C:/myFiles/company_project/xbot/mytest/test.py", line 39, in <module>
    #     exc_coro.throw(ZeroDivisionError)  # 如果无法处理传入的异常,协程会终止
    #   File "C:/myFiles/company_project/xbot/mytest/test.py", line 13, in demo_exc_handling
    #     x = yield
    # ZeroDivisionError
    print(getgeneratorstate(exc_coro))
    # GEN_CLOSED

    exc_coro.close()
    print(getgeneratorstate(exc_coro))
    # GEN_CLOSED

如果不管协程如何结束都想做些清理工作,要把协程定义体相关的代码放入 try/finally 块中, 如下示例。

# -*- coding: utf-8 -*-
from inspect import getgeneratorstate


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')