协程中未处理的异常
会向上抛出
,传给 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
变量上。
上面示例,暗示了
终止协程的一种方式
:发送某个哨符值
,让协程
退出。内置的 None
和 Ellipsis
等常量经常用作哨符值。Ellipsis
的优点
是,数据流中不太常有
这个值。我还见过有人把 StopIteration
类(类本身,而不是实例,也不抛出)作为哨符值;也就是说,是像这样使用的:my_coro.send(StopIteration)
。
从 Python 2.5
开始,客户代码可以在生成器对象上调用两个方法
,显式地
把异常发给协程。这两个方法是 throw
和 close
。
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
)。
下面举例说明如何使用 close
和 throw
方法控制协程。
# -*- 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')