yield如何捕获StopIteration异常?

时间:2013-05-09 15:20:31

标签: python yield stopiteration

为什么示例函数终止:

def func(iterable):
    while True:
        val = next(iterable)
        yield val

但如果我脱掉yield语句函数会引发StopIteration异常吗?

编辑:很抱歉误导你们。我知道什么是发电机以及如何使用它们。当然,当我说功能终止时,我并不意味着急切的功能评估。我只是暗示当我使用函数生成生成器时:

gen = func(iterable)

func 的情况下,它可以工作并返回相同的生成器,但是在func2的情况下:

def func2(iterable):
    while True:
        val = next(iterable)

它引发了StopIteration而不是 None 返回或无限循环。

让我更具体一点。 itertools 中有一个 tee 函数,相当于:

def tee(iterable, n=2):
    it = iter(iterable)
    deques = [collections.deque() for i in range(n)]
    def gen(mydeque):
        while True:
            if not mydeque:             # when the local deque is empty
                newval = next(it)       # fetch a new value and
                for d in deques:        # load it to all the deques
                    d.append(newval)
            yield mydeque.popleft()
    return tuple(gen(d) for d in deques)

事实上,有一些魔力,因为嵌套函数 gen 具有无限循环而没有break语句。当 中没有项目时, gen 函数因 StopIteration 异常而终止。但它正确终止(没有引发异常),即只是停止循环。 所以问题是:处理 StopIteration 的位置在哪里?

5 个答案:

答案 0 :(得分:38)

要回答有关StopIterationgen内创建的itertools.tee生成器中被捕获的问题:它没有。 tee结果的消费者可以在迭代时捕获异常。

首先,重要的是要注意生成器函数(任何地方都带有yield语句的任何函数)与普通函数根本不同。而不是在调用函数时运行函数代码,而是在调用函数时只获得generator对象。只有当您遍历生成器时,才会运行代码。

生成器函数永远不会在没有引发StopIteration的情况下完成迭代(除非它引发一些其他异常)。 StopIteration是生成器发出的信号,它不是可选的。如果你在没有提出任何内容的情况下到达return语句或生成器函数代码的末尾,Python将为你引发StopIteration

这与常规函数不同,如果它们到达末尾而不返回任何其他内容,则返回None。如上所述,它与发电机工作的不同方式联系在一起。

这是一个示例生成器函数,可以很容易地看到StopIteration如何被引发:

def simple_generator():
    yield "foo"
    yield "bar"
    # StopIteration will be raised here automatically

以下是您使用它时会发生的事情:

>>> g = simple_generator()
>>> next(g)
'foo'
>>> next(g)
'bar'
>>> next(g)
Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    next(g)
StopIteration

调用simple_generator总是立即返回generator对象(不运行函数中的任何代码)。生成器对象上的next的每次调用都会运行代码,直到下一个yield语句,并返回生成的值。如果没有更多内容,则会引发StopIteration

现在,通常您看不到StopIteration例外情况。原因是您通常在for循环内使用生成器。 for语句会反复自动调用next,直到StopIteration被提升为止。它将为您捕获并抑制StopIteration例外,因此您无需使用try / except块来处理它。

for这样的for item in iterable: do_suff(item)循环几乎完全等同于此while循环(唯一的区别是真正的for不需要临时变量来保存迭代器):

iterator = iter(iterable)
try:
    while True:
        item = next(iterator)
        do_stuff(item)
except StopIteration:
    pass
finally:
    del iterator

您在顶部显示的gen生成器函数是一个例外。它使用它正在消耗的迭代器产生的StopIteration异常,因为它是自己的迭代信号。也就是说,它不是捕获StopIteration然后突破循环,而是简单地让异常没有被捕获(可能被某些更高级别的代码捕获)。

与主要问题无关,我还想指出另一件事。在您的代码中,您在名为next的变量上调用iterable。如果您将该名称作为您将获得的对象类型的文档,则不一定安全。

nextiterator协议的一部分,而不是iterable(或container)协议。它可能适用于某些类型的迭代(例如文件和生成器,因为这些类型是它们自己的迭代器),但是对于其他迭代(例如元组和列表)它将失败。更正确的方法是在iter值上调用iterable,然后在收到的迭代器上调用next。 (或者只使用for循环,在适当的时间为您调用iternext!)

编辑:我刚刚在谷歌搜索中找到了我自己的答案中的一个相关问题,我想我会更新以指出上述答案在未来的Python版本中并不完全正确。 PEP 479允许StopIteration从生成器函数中冒出来,这是一个错误。如果发生这种情况,Python会将其转换为RuntimeError例外。

这意味着需要修改类似itertools中使用StopIteration来突破生成器函数的示例的代码。通常,您需要使用try / except来捕获异常,然后执行return

因为这是一个向后不兼容的变化,所以它逐渐被逐步推进。在Python 3.5中,默认情况下所有代码都将像以前一样工作,但您可以使用from __future__ import generator_stop获取新行为。在Python 3.6中,代码仍然有效,但会发出警告。在Python 3.7中,新行为将始终适用。

答案 1 :(得分:6)

当一个函数包含yield时,调用它实际上并不执行任何操作,它只是创建一个生成器对象。只有遍历此对象才会执行代码。所以我的猜测是你只是调用函数,这意味着函数不会引发StopIteration,因为它永远不会被执行。

鉴于你的功能和可迭代:

def func(iterable):
    while True:
        val = next(iterable)
        yield val

iterable = iter([1, 2, 3])

这是错误的称呼方式:

func(iterable)

这是正确的方法:

for item in func(iterable):
    # do something with item

您还可以将生成器存储在变量中并在其上调用next()(或以其他方式迭代它):

gen = func(iterable)
print(next(gen))   # prints 1
print(next(gen))   # prints 2
print(next(gen))   # prints 3
print(next(gen))   # StopIteration

顺便说一下,编写函数的更好方法如下:

def func(iterable):
    for item in iterable:
        yield item

或者在Python 3.3及更高版本中:

def func(iterable):
    yield from iter(iterable)

当然,真正的发电机很少是如此微不足道。 : - )

答案 2 :(得分:3)

如果没有yield,您将遍历整个iterable,而不会停止对val执行任何操作。 while循环不会捕获StopIteration异常。等效的for循环将是:

def func(iterable):
    for val in iterable:
        pass

它会捕获StopIteration并简单地退出循环,从而从函数返回。

您可以明确捕获异常:

def func(iterable):
    while True:
        try:
            val = next(iterable)
        except StopIteration:
            break

答案 3 :(得分:0)

yield无法抓住StopIterationyield为您的函数做的是它使它成为生成函数而不是常规函数。因此,从函数调用返回的对象是一个可迭代对象(当您使用next函数(通过for循环隐式调用)时,它会计算下一个值)。如果你将yield语句从中删除,那么python会立即执行整个while循环,最终耗尽迭代(如果它是有限的)并在你调用时向右提升StopIteration

考虑:

x = func(x for x in [])
next(x)  #raises StopIteration

for循环捕获异常 - 这就是它知道何时停止在你给它的迭代上调用next

答案 4 :(得分:0)

在Python 3.8上测试,块作为惰性生成器

def split_to_chunk(size: int, iterable: Iterable) -> Iterable[Iterable]:
    source_iter = iter(iterable)
    while True:
        batch_iter = itertools.islice(source_iter, size)
        try:
            yield itertools.chain([next(batch_iter)], batch_iter)
        except StopIteration:
            return

为什么要处理StopInteration错误:https://www.python.org/dev/peps/pep-0479/

def sample_gen() -> Iterable[int]:
    i = 0
    while True:
        yield i
        i += 1

for chunk in split_to_chunk(7, sample_gen()):
    pprint.pprint(list(chunk))
    time.sleep(2)

输出:

[0, 1, 2, 3, 4, 5, 6]
[7, 8, 9, 10, 11, 12, 13]
[14, 15, 16, 17, 18, 19, 20]
[21, 22, 23, 24, 25, 26, 27]
............................