使用生成器表达式导致 Python 挂起

Using generator expression causes Python to hang

我正在试验两个模拟 Python 2.x 和 3.x 中内置的 zip 的函数。第一个 returns 一个列表(如 Python 2.x),第二个是生成器函数,一次 returns 它的一个结果集(如Python 3.x):

def myzip_2x(*seqs):
    its = [iter(seq) for seq in seqs]
    res = []
    while True:
        try:
            res.append(tuple([next(it) for it in its]))   # Or use generator expression?
            # res.append(tuple(next(it) for it in its))
        except StopIteration:
            break
    return res

def myzip_3x(*seqs):
    its = [iter(seq) for seq in seqs]
    while True:
        try:
            yield tuple([next(it) for it in its])         # Or use generator expression?
            # yield tuple(next(it) for it in its)
        except StopIteration:
            return

print(myzip_2x('abc', 'xyz123'))                   
print(list(myzip_3x([1, 2, 3, 4, 5], [7, 8, 9])))

这很好用并给出了 zip 内置的预期输出:

[('a', 'x'), ('b', 'y'), ('c', 'z')]
[(1, 7), (2, 8), (3, 9)]

然后我考虑用它的(几乎)等效的生成器表达式替换 tuple() 调用中的列表理解,通过删除方括号 [] (为什么创建一个临时列表时使用理解生成器应该可以满足 tuple() 预期的迭代,对吧?)

但是,这会导致 Python 挂起。如果未使用 Ctrl C 终止执行(在 Windows 上处于 IDLE 状态),它最终会在几分钟后停止并显示一个 (预期)MemoryError 异常。

调试代码(例如使用 PyScripter)显示使用生成器表达式时从未引发 StopIteration 异常。上面对 myzip_2x() 的第一个示例调用继续向 res 添加空元组,而对 myzip_3x() 的第二个示例调用产生元组 (1, 7)(2, 8)(3, 9), (4,), (5,), (), (), (), ....

我是不是漏掉了什么?

最后一点:如果 its 在每个函数的第一行变成一个生成器(使用 its = (iter(seq) for seq in seqs))(当在 tuple()打电话)。

编辑:

感谢@Blckknght 的解释,你是对的。 This message 使用与上述生成器函数类似的示例提供了有关正在发生的事情的更多详细信息。总之,像这样使用生成器表达式仅适用于 Python 3.5+,它需要在文件顶部使用 from __future__ import generator_stop 语句并将 StopIteration 更改为上面的 RuntimeError(再次,当使用生成器表达式而不是列表理解时)。

编辑 2:

至于上面的最后一点:如果 its 成为一个生成器(使用 its = (iter(seq) for seq in seqs)),它将只支持一次迭代——因为生成器是一次性迭代器。因此 while 循环第一次耗尽 运行 并且在后续循环中仅获得空元组。

当你这样做时:

tuple([next(it) for it in its])

您首先创建了一个列表,然后将其传递给 tuple()。如果由于引发 StopIteration 而无法创建列表,则不会创建列表并传播异常。

但是当你这样做时:

tuple(next(it) for it in its)

您正在构造一个生成器并将其直接传递给 tuple()。元组构造函数将生成器用作迭代器:即,将查看项目直到引发 StopIteration

StopIterationtuple()捕获,不传播

立即引发 StopIteration 的生成器被转换为空元组。

我不太确定,但看起来你有嵌套的生成器和外部的捕获器 StopIteration 由内部引发。

考虑这个例子:

def gen(its):
    for it in its:
        yield next(it)  # raises StopIteration

tuple(gen(its))  # doesn't raises StopIteration

它的功能与您的版本相同。

以下是基于这些代码的运行时行为的猜测,而不是 Python 语言参考或参考实现。

表达式 tuple(next(it) for it in its) 等价于 tuple(generator) 其中 generator = (next(it) for it in its)tuple 构造函数在概念上 等同于以下代码:

def __init__(self, generator):
    for element in generator:
        self.__internal_array.append(element)

因为 for 语句捕获任何 StopIteration 作为耗尽的标志,当生成器引发 StopIteration 因为 next(it) 引发它时,for语句将简单地捕获它并认为生成器已耗尽。这就是循环永不结束并附加空元组的原因:异常永远不会在 tuple 构造函数中冒泡。

另一方面,列表理解 [next(it) for it in its] 在概念上 等同于

result = []
for it in its:
    result.append(next(it))

所以 StopIteration 没有被 for 语句捕获。

这个例子展示了文字理解和使用生成器表达式调用构造函数之间一个有趣的重要区别。如果使用 list(next(it) for it in its vs [next(it) for it in its].

也会发生同样的情况

您看到的行为是一个错误。它源于这样一个事实,即从生成器中冒出的 StopIteration 异常与正常退出的生成器无法区分。这意味着您不能用 tryexcept 在生成器上包装一个循环并寻找 StopIteration 来使您跳出循环,因为循环逻辑将消耗异常。

PEP 479 提出了该问题的解决方案,通过更改语言使生成器中未捕获的 StopIteration 在冒泡之前变成 RuntimeError。这将允许您的代码工作(对您捕获的异常类型进行小的调整)。

PEP 已在 Python 3.5 中实现,但为了保持向后兼容性,更改后的行为仅在您通过将 from __future__ import generator_stop 放在文件顶部来请求时才可用。 Python 3.7 将默认启用新行为(Python 3.6 将默认为旧行为,但如果出现这种情况可能会发出警告)。