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

时间:2015-10-05 07:33:21

标签: python generator list-comprehension

我正在尝试使用2个函数来模拟Python 2.x和3.x中内置的zip。第一个返回一个列表(如Python 2.x),第二个返回一个生成器函数,一次返回一个结果集(如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异常。< / p>

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

我错过了什么吗?

最后一点:如果...在每个函数的第一行中成为生成器(使用its),则显示相同的挂起行为(当its = (iter(seq) for seq in seqs)中使用列表推导时呼叫)。

编辑:

感谢@Blckknght的解释,你是对的。 This message提供了有关使用上述生成器函数的类似示例所发生情况的更多详细信息。总之,使用像这样的生成器表达式只能在Python 3.5+中运行,并且需要在文件顶部使用tuple()语句并在from __future__ import generator_stop上方更改StopIteration(同样,在使用生成器时)表达而不是列表推导)。

编辑2:

至于上面的最后一点:如果RuntimeError成为生成器(使用its),它将只支持一次迭代 - 因为生成器是一次性迭代器。因此,第一次运行while循环时它会耗尽,而在后续循环中只会获得空元组。

4 个答案:

答案 0 :(得分:2)

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

表达式tuple(generator)相当于generator = (next(it) for it in its) tupledef __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]构造函数冒泡。

另一方面,列表理解{em>概念等同于

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],则会发生相同的情况。

答案 1 :(得分:2)

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

PEP 479建议修复此问题,方法是更改​​语言,使生成器内的未被捕获的StopIteration在冒泡之前变为RuntimeError。这将允许您的代码工作(通过对您捕获的异常类型进行小调整)。

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

答案 2 :(得分:0)

当你这样做时:

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

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

但是当你这样做时:

tuple(next(it) for it in its)

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

也就是说,StopIterationtuple()捕获,并且不会传播。

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

答案 3 :(得分:0)

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

考虑这个例子:

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

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

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