【问题标题】:Generator expression never raises StopIteration生成器表达式从不引发 StopIteration
【发布时间】:2013-05-24 17:26:14
【问题描述】:

受到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中的行为相同

【问题讨论】:

  • 也受到了我的 cmets 的启发。我知道,我现在很有名。

标签: python iterator generator


【解决方案1】:

开发人员认为允许这样做是一个错误,因为它可以掩盖晦涩的错误。因此,接受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循环:

for a in b:
    # do stuff

作为(大致)等价于:

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

现在,所有示例都有 两个 for 循环嵌套在一起(一个在生成器表达式中,一个在使用它的函数中),因此当外部循环执行其 @ 时,内部循环迭代一次987654329@电话。当内部循环中的“#do stuff”为raise StopIteration 时会发生什么?

>>> def foo(): raise StopIteration
>>> list(foo() for x in range(10))
[]

异常从内部循环传播出去,因为它不在其保护范围内,并被外部循环捕获。在新的行为下,Python 将拦截一个即将从生成器传播出来的StopIteration,并将其替换为RuntimeError,它不会被包含for 循环捕获。

这也暗示了这样的代码:

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

这在某种程度上是一个实现细节泄漏 - 列表推导不要转换为使用等效生成器表达式对list 的调用,显然这样做would cause large performance penalties 是考虑禁止。

【讨论】:

  • 非常有趣 - 也令人困惑。感谢您指出与 Py3 的“不一致”并列出 comps...
  • 您的最后一个示例传播了异常,因为它在调用any 之前建立了列表。输入 any 后,[] 中的任何内容都不会被评估。
  • 很好的答案注意到 PEP479 实际上现在确实引发了RuntimeError: generator raised StopIteration,验证了我对旧 Python 中的行为的惊讶。我认为您的 itertools 魔术有点过头了,但是我可以简单地将 next(it) 更改为 next(it, None) 并且效果很好。随意用这个更简单的解决方案替换你的那部分答案
【解决方案2】:

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

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

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)) # --> []

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

【讨论】:

  • 这不太一样 - 由生成器引发的 StopIteration 将始终出现在循环自己对 next 的调用中,而永远不会出现在循环的有效主体中。试试for _ in range(10): raise StopIteration - 你会得到回溯。
  • 啊,这完全有道理,我想我的函数F 是一秒钟的生成器
  • @lvc 你会得到一个回溯,是的 - 但如果在生成器中使用,它将在这个上下文中被捕获。
  • @glglgl 它将被任何迭代生成器结果的东西捕获,而不是被生成器本身捕获。这解释为什么any 似乎会吞下它(它会发生在anynext 的调用和信号'正常迭代器结束') -除了列表推导,尽管(尤其是在 Python 3 中)表现为list(genexp) 的语法糖。而且,特别是,为什么any 会从 listcomp 而不是 genexp 传播异常 - 这意味着它是 generator 本身 提前结束,而不是任何消耗它的东西。
  • @glglgl 我已经编辑了自己的答案,以增加对我的观点的不那么狭窄的讨论。
猜你喜欢
  • 1970-01-01
  • 2021-07-30
  • 1970-01-01
  • 1970-01-01
  • 2018-03-26
  • 2021-11-01
  • 1970-01-01
  • 2020-01-06
  • 2017-12-08
相关资源
最近更新 更多