【问题标题】:Understanding StopIteration handling inside generators for non-trivial case了解生成器内部的 StopIteration 处理非平凡情况
【发布时间】:2019-04-02 10:39:38
【问题描述】:

我正在帮助维护一些现在包含自动化 Python 3.7 测试的代码。这导致我遇到了一些与PEP 479“更改生成器内部的停止迭代处理”相关的问题。我幼稚的理解是,您可以使用 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]

【问题讨论】:

    标签: python python-3.x exception generator stopiteration


    【解决方案1】:

    前两个案例在班级的__init__ 中提出了一个未捕获的StopIterationlist 构造函数在 Python 3.6 中处理得很好(可能会出现警告,具体取决于版本)。然而,异常传播之前wrapper 有机会迭代:有效失败的行是lst = MyClass(),循环for item in lst: 永远不会运行,导致生成器为空。 p>

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

    DeprecationWarning: generator 'wrapper' raised StopIteration
    

    这里的结论是双重的:

    1. 不要让迭代器自行耗尽。检查它何时停止是你的工作。使用for 循环很容易做到这一点,但必须使用while 循环手动完成。案例A 就是一个很好的例证。
    2. 不要重新引发内部异常。改为返回None。案例B 只是不是要走的路。 breakreturn 可以在 except 块中正常工作,就像您在 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 的调用,但即使是这些情况也可以用嵌套循环重写)。

    【讨论】:

    • 有趣的是我没有收到 Python 3.6.6 的警告;但我认为你的解释基本上是正确的,谢谢!我想这就是他们引入 PEP 来阐明此类错误的原因!
    • @Chris_Rands。这就是为什么即使手动调用iter 我仍然更喜欢for 循环。它基本上是选项C 中的try-catch 块的语法糖。节省大量输入,并且考虑到流程的性质,您非常很少需要其他任何内容。
    • 在这种情况下,我的真实例子并不像我展示的那样做作,但总的来说我同意
    • “列表包装器在 Python 3.6 中处理得很好”——要清楚,不是列表包装器处理它吗?这只是因为yield 将整个事情变成了一个生成器函数(但是你是对的,生成器在lst = MyClass() 行终止),是吗?
    • (没有yield 仍然会有StopIteration 提出)
    猜你喜欢
    • 2016-10-08
    • 2017-12-27
    • 2013-12-19
    • 1970-01-01
    • 2021-11-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-27
    相关资源
    最近更新 更多