next()函数在Python中与any/all配合使用时存在问题。

26

今天我发现了一个错误,因为我在使用next()方法提取一个值时,当值不存在时会抛出StopIteration的异常。

通常情况下,这会停止程序。但是,在一个all()迭代中调用使用next的函数时,all只是提前终止并返回True

这是预期行为吗?有没有样式指南可以帮助避免这种情况?

以下是简化的示例:

def error(): return next(i for i in range(3) if i==10)
error() # fails with StopIteration
all(error() for i in range(2)) # returns True

9
当iterable中所有元素均为True(或iterable为空)时返回True。 - Frédéric Hamidi
5
因为 any 函数返回的是可迭代对象中包含任何一个值为 True 时即返回 True,而 all 函数则在可迭代对象中不包含任何一个 False 值时才返回 True - Adam Smith
2
所以说,集合中的所有项目都是真实的(因为没有任何项目),但没有一个单独的项目是真实的(因为没有任何项目)。 - tdelaney
1
next((i for i in range(3) if i==10), None) 将返回 None 而不是抛出 StopIteration 异常。 - Peter Wood
3
@FrédéricHamidi - 嗯,那就是我说的。 - tdelaney
显示剩余8条评论
2个回答

24

在Python 3.6及以下版本中,这是默认行为,但被认为是语言错误,计划在Python 3.7中更改为引发异常。

PEP 479所述:

生成器和StopIteration的交互目前有些令人惊讶,并且可能掩盖晦涩的错误。不应该出现意外异常导致细微的行为变化,而应该引起响亮且易于调试的回溯。当前,在生成器函数内意外抛出的StopIteration将被解释为驱动生成器的循环构造的迭代结束。

从Python 3.5开始,可以将默认行为更改为预定在3.7中的行为。这段代码:

# gs_exc.py

from __future__ import generator_stop

def error():
    return next(i for i in range(3) if i==10)

all(error() for i in range(2))

…引发以下异常:

Traceback (most recent call last):
  File "gs_exc.py", line 8, in <genexpr>
    all(error() for i in range(2))
  File "gs_exc.py", line 6, in error
    return next(i for i in range(3) if i==10)
StopIteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "gs_exc.py", line 8, in <module>
    all(error() for i in range(2))
RuntimeError: generator raised StopIteration

在 Python 3.5 和 3.6 中,如果没有导入 __future__,则会产生警告。例如:

# gs_warn.py

def error():
    return next(i for i in range(3) if i==10)

all(error() for i in range(2))

$ python3.5 -Wd gs_warn.py 
gs_warn.py:6: PendingDeprecationWarning: generator '<genexpr>' raised StopIteration
  all(error() for i in range(2))

$ python3.6 -Wd gs_warn.py 
gs_warn.py:6: DeprecationWarning: generator '<genexpr>' raised StopIteration
  all(error() for i in range(2))

谢谢 - 我一直抱着希望,被StopIteration冒泡惊到没有让我发疯。 - amwinter
这是否意味着 all(some_iterator)mlist = [i for i in some_iterator];all(mlist) 的行为会有所不同? - tdelaney
@tdelaney 是的,如果 some_iterator 是一个会引发 StopIteration 的生成器。 - Zero Piraeus

9
问题不在于使用all,而是你将生成器表达式作为all的参数。 StopIteration 会传播到生成器表达式中,它不知道它从哪里来,所以它会按照惯例结束迭代。
您可以通过将error函数替换为直接引发错误的内容来看到这一点:
def error2(): raise StopIteration

>>> all(error2() for i in range(2))
True

最后一个谜题的答案是了解当传入一个空序列时,all 的行为:

>>> all([])
True

如果你要直接使用next,你需要准备好自己捕获StopIteration异常。
编辑:很高兴看到Python开发者认为这是一个bug,并且正在采取措施在3.7中进行更改。

1
好的 - 你的意思是不在任何可能引发StopIteration异常的地方使用any/all,这样就可以解决问题了? - amwinter
@PadraicCunningham 它被生成器表达式本身捕获。 - Mark Ransom
OP 可以捕获异常或使用 next 的默认值。 - jamylak
@amwinter,不仅如此 - 建议是不要在生成器表达式内调用可能引发StopIteration的任何内容。 - Mark Ransom
不确定为什么这个被踩了 - 这是对当前情况的很好的解释。 - Zero Piraeus
是我点了踩,但那完全是个意外...我一定是在光标停留在踩按钮上的时候不小心点击了鼠标。我刚刚才注意到这件事,但时间已经过去太久了,无法撤销。@MarkRansom 如果你进行编辑,我就可以撤销我的意外踩了。 - SethMMorton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接