了解非平凡情况下生成器内部的StopIteration处理



我正在帮助维护一些代码,这些代码现在包括自动化的Python 3.7测试。这让我想到了一些与PEP479"生成器内部的ChangeStopIteration处理"相关的问题。我天真的理解是,你可以使用try-except块来修改旧代码,使其与所有python版本兼容,例如

旧代码:

def f1():
it = iter([0])
while True:
yield next(it)
print(list(f1()))
# [0] (in Py 3.6)
# "RuntimeError: generator raised StopIteration" (in Py 3.7;
# or using from __future__ import generator_stop)

成为:

def f2():
it = iter([0])
while True:
try:
yield next(it)
except StopIteration:
return 
print(list(f2()))
# [0] (in all Python versions)

对于这个微不足道的例子,它是有效的,但我发现对于一些更复杂的代码,我正在重新分解它,但它不是。以下是Py 3.6的一个最小示例:

class A(list):
it = iter([0])
def __init__(self):
while True:
self.append(next(self.it))
class B(list):
it = iter([0])
def __init__(self):
while True:
try:
self.append(next(self.it))
except StopIteration:
raise
class C(list):
it = iter([0])
def __init__(self):
while True:
try:
self.append(next(self.it))
except StopIteration:
return  # or 'break'
def wrapper(MyClass):
lst = MyClass()
for item in lst:
yield item
print(list(wrapper(A)))
# [] (wrong output)
print(list(wrapper(B)))
# [] (wrong output)
print(list(wrapper(C)))
# [0] (desired output)

我知道AB的例子是完全等价的,C的情况是与Python 3.7兼容的正确方式(我也知道,对for循环的重新分解对于许多例子来说都是有意义的,包括这个人为的例子(。

但问题是,为什么具有AB的示例会产生一个空列表[],而不是[0]

前两种情况在类的__init__中引发了一个未捕获的StopIterationlist构造函数在Python 3.6中处理得很好(根据版本不同,可能会有警告(。然而,异常在wrapper获得迭代机会之前传播:有效失败的行是lst = MyClass(),循环for item in lst:从不运行,导致生成器为空。

当我在Python 3.6.4中运行此代码时,我在print行(对于AB(上都收到以下警告:

DeprecationWarning: generator 'wrapper' raised StopIteration

这里的结论有两个:

  1. 不要让迭代器自己用完。你的工作是检查它何时停止。使用for循环很容易做到这一点,但必须使用while循环手动完成。案例A就是一个很好的例子
  2. 不要重新引发内部异常。改为返回None。病例B不是我们要走的路。breakreturnexcept块中可以正常工作,就像在C中一样

考虑到for循环是C中try-except块的语法糖,我通常建议使用它们,即使手动调用iter:也是如此

class D(list):
it = iter([0])
def __init__(self):
for item in it:
self.append(item)

这个版本在功能上相当于C,为您做所有的记账工作。很少有情况需要实际的while循环(会想到跳过对next的调用,但即使是这些情况也可以用嵌套循环重写(。

最新更新