Python 生成器结束时报 RuntimeError: generator raised StopIteration
Python 3.7 起,生成器内部漏出的 StopIteration 会转换成 RuntimeError。用 return 正常结束,并在生成器内处理预期的 next 耗尽,保留普通迭代器协议和真实业务错误。
生成器已经产出预期数据,循环到末尾却报错:
RuntimeError: generator raised StopIteration
检查异常链,如果前一个异常是生成器内部显式抛出的 StopIteration,问题通常出在结束方式。Python 3.7 起,未处理的 StopIteration 即将离开生成器时会被转换为 RuntimeError,而不会被当成正常结束。本篇针对同步生成器;普通迭代器的 __next__() 协议并未因此取消。
用 return 表达生成器正常结束
下面的写法把本应正常完成的生成器变成了异常退出:
def numbers():
yield from range(3)
raise StopIteration
消费方调用 list(numbers()) 时会一直推进生成器,直到执行最后一行。即使此前已经产出了 0、1、2,末尾的异常仍会导致这次列表构造失败。只调用 numbers() 得到生成器对象,还不足以触发这条路径。
若目的就是结束生成器,将显式异常改为 return:
def numbers():
yield from range(3)
return
这一版本的 list(numbers()) 预期得到 [0, 1, 2]。在函数末尾,直接省略 return、自然走到结尾也可以;若是中途达到结束条件,则保留明确的 return。正常返回的生成器仍会向迭代协议报告耗尽,但不会触发上述异常转换。
不要把整个项目中的 raise StopIteration 批量替换为 return。手写普通迭代器的 __next__() 在没有下一项时仍需要抛出 StopIteration;普通函数的 return 也只能结束它自身,不能替调用它的生成器完成退出。
没有显式 raise,也要检查内部 next
同样的报错还可能来自生成器中的 next()。例如下面这种“不断取下一项”的结构,在输入耗尽时会把 StopIteration 传到生成器外:
def relay(values):
iterator = iter(values)
while True:
yield next(iterator)
如果只是逐项转交,用正常迭代结构即可处理输入结束:
def relay(values):
for item in values:
yield item
复杂逻辑必须手动取下一项时,只围住预期会耗尽的位置,并在生成器内结束:
def relay(values):
iterator = iter(values)
while True:
try:
item = next(iterator)
except StopIteration:
return
yield item
这里把“输入耗尽”定义为正常完成。若业务要求每条记录必须还有后续字段,提前耗尽可能表示数据损坏,不能也转成成功结束;应按接口约定报告错误。检查异常链可以区分故意的结束语句与意外耗尽,不应为了少一个报错就扩大捕获范围。
消费方原来的 StopIteration 处理仍有用途
生成器正常耗尽后,外部直接调用 next() 仍会收到 StopIteration,for 循环仍据此结束。改变的是生成器内部漏出的异常,而不是所有位置的 StopIteration 都改了类型。
例如,消费一个空的普通列表迭代器仍可这样处理:
iterator = iter([])
try:
item = next(iterator)
except StopIteration:
print("finished")
这段代码没有让异常穿过生成器边界。若真实代码外层捕获 StopIteration 却仍出现 RuntimeError,应修复生成器内部的来源,而不是把外层改成捕获所有 RuntimeError 后直接忽略。后者还会掩盖其他运行错误,也无法让已经异常退出的生成器从原位置继续。
对照版本与完整消费结果
Python 3.5、3.6 可以通过 from __future__ import generator_stop 启用这一行为;3.7 起已默认启用。新版本里删除这条历史导入不能恢复旧语义。旧代码升级后开始报错,也不能据此认定产生数据的部分全部失效,应检查异常实际发生的位置。
修复后要完整消费生成器,分别验证空输入、正常多项输入、提前结束条件,以及业务处理本身抛出异常的情况。确认输出项数和值符合预期,真正的业务错误仍能传给调用方。只检查第一项,可能完全触及不到原来的结束故障。
保留修改前的代码和依赖锁定记录。涉及数据库写入、发送消息等副作用时,即使消费最终失败,此前已经产出的项目也可能被处理过;重试前先核对处理进度,不能假定报错意味着整轮没有执行。回退代码不会撤销这些操作,也不应通过吞掉 RuntimeError 把不完整结果标成成功。


