生成器表达式永远不会引发StopIteration

时间:2013-05-29 12:38:37

标签: python iterator generator

受到my own answer的启发,我甚至不了解它是如何运作的,请考虑以下因素:

def has22(nums):
    it = iter(nums)
    return any(x == 2 == next(it) for x in it)


>>> has22([2, 1, 2])
False

我期望引发StopIteration,因为在到达2时,next(it)将推进消耗的迭代器。但是,对于生成器表达式,似乎已完全禁用此行为!一旦发生这种情况,生成器表达似乎立即break

>>> it = iter([2, 1, 2]); any(x == 2 == next(it) for x in it)
False
>>> it = iter([2, 1, 2]); any([x == 2 == next(it) for x in it])

Traceback (most recent call last):
  File "<pyshell#114>", line 1, in <module>
    it = iter([2, 1, 2]); any([x == 2 == next(it) for x in it])
StopIteration
>>> def F(nums):
        it = iter(nums)
        for x in it:
            if x == 2 == next(it): return True


>>> F([2, 1, 2])

Traceback (most recent call last):
  File "<pyshell#117>", line 1, in <module>
    F([2, 1, 2])
  File "<pyshell#116>", line 4, in F
    if x == 2 == next(it): return True
StopIteration

即使这样也有效!

>>> it=iter([2, 1, 2]); list((next(it), next(it), next(it), next(it))for x in it)
[]

所以我想我的问题是,为什么这个行为启用了生成器表达式?

注意: 3.x 中的相同行为

2 个答案:

答案 0 :(得分:5)

开发人员已经决定允许这是一个错误,因为它可以掩盖晦涩的错误。因此,接受PEP 479意味着这种情况正在消失。

在Python 3.5中如果执行from __future__ import generator_stop,默认情况下在Python 3.7中,问题中的示例将失败并显示RuntimeError。你仍然可以通过一些itertools魔法达到同样的效果(允许nums不被预先计算):

from itertools import tee, islice

def has22(nums):
    its = tee(nums, 2)
    return any(x == y == 2 for x, y in 
               zip(its[0], islice(its[1], 1, None)))

它首先起作用的原因与发电机的工作方式有关。你可以想到这个循环:

for a in b:
    # do stuff

与(大致)相当于:

b = iter(b) 
while True:
    try:
        a = next(b)
    except StopIteration:
        break
    else:
        # do stuff

现在,所有示例都有两个 for循环嵌套在一起(一个在生成器表达式中,一个在函数中使用它),因此当外循环执行它时,内循环迭代一次{ {1}}致电。当内循环中的'#do stuff'为next时会发生什么?

raise StopIteration

异常传播出内部循环,因为它不在其保护范围内,并被外部循环捕获。在新行为下,Python将拦截即将从生成器传播的>>> def foo(): raise StopIteration >>> list(foo() for x in range(10)) [] 并将其替换为StopIteration将不会被包含的捕获for循环。

这也暗示了这样的代码:

RuntimeError

也会失败,邮件列表主题给人的印象是,无论如何这被视为不良形式。正确的方法是:

def a_generator():
     yield 5
     raise StopIteration

正如您所指出的,列表推导已经表现得不同:

def a_generator():
    yield 5
    return

这有点是一个实现细节泄漏 - 列表推导没有转换为对具有等效生成器表达式的>>> [foo() for x in range(10)] Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 1, in <listcomp> File "<stdin>", line 1, in foo StopIteration 的调用,并且显然这样做would cause large performance penalties被认为是禁止的权力。

答案 1 :(得分:4)

有趣的行为,但绝对可以理解。

如果将生成器表达式转换为生成器:

def _has22_iter(it):
    for x in it:
        yield x == 2 and x == next(it)

def has22(nums):
    it = iter(nums)
    return any(_has22_iter(it))

您的生成器在以下条件中引发StopIteration

  • 生成器功能到达终点
  • 某处有return语句
  • 某处有raise StopIteration

这里,你有第三个条件,所以生成器终止了。

与以下内容比较:

def testgen(x):
    if x == 0:
        next(iter([])) # implicitly raise
    if x == 1:
        raise StopIteration
    if x == 2:
        return

并做

list(testgen(0)) # --> []
list(testgen(1)) # --> []
list(testgen(2)) # --> []
list(testgen(3)) # --> []

在所有情况下都会得到相同的行为。