13

受到我自己的回答的启发,我什至自己都不明白它是如何工作的,请考虑以下几点:

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


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

我预计 aStopIteration会被提出,因为在达到 时2next(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

4

2 回答 2

6

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

在 Python 3.5 中,如果您这样做from __future__ import generator_stop,并且在默认情况下在 Python 3.7 中,问题中的示例将失败并显示RuntimeError. 您仍然nums可以使用一些 itertools 魔法实现相同的效果(允许不被预先计算):

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 循环(一个在生成器表达式中,一个在使用它的函数中),因此当外部循环执行其next调用时,内部循环迭代一次。当内部循环中的“#do stuff”为 时会发生什么raise StopIteration

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

异常从内部循环传播出去,因为它不在其保护范围内,并被外部循环捕获。在新行为下,Python 将拦截StopIteration即将从生成器传播出来的 a 并将其替换为 a 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具有等效生成器表达式的调用,并且显然这样做会导致巨大的性能损失,而这些能力被认为是禁止的。

于 2013-05-29T12:48:26.647 回答
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)) # --> []

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

于 2013-05-29T12:45:06.760 回答