【问题标题】:How yield catches StopIteration exception?yield 如何捕获 StopIteration 异常?
【发布时间】:2013-05-04 03:18:24
【问题描述】:

为什么在示例函数中终止:

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 函数在 it 中没有项目时由于 StopIteration 异常而终止。但它正确终止(不引发异常),即只是停止循环。 所以问题是StopIteration 是在哪里处理的?

【问题讨论】:

  • 你怎么称呼这个?

标签: python yield stopiteration


【解决方案1】:

注意:这个问题(以及我对它的回答的原始部分)只对 3.7 之前的 Python 版本真正有意义。由于PEP 479 中描述的更改,所询问的行为在 3.7 及更高版本中不再发生。因此,这个问题和原始答案仅作为历史文物才真正有用。在 PEP 被接受后,我在答案底部添加了一个与现代 Python 版本更相关的部分。


要回答您关于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 item in iterable: do_suff(item) 这样的for 循环几乎完全等同于while 循环(唯一的区别是真正的for 不需要临时变量来保存迭代器):

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

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

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

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


我刚刚在 Google 搜索相关问题时找到了自己的答案,我觉得我应该更新以指出上述答案在现代 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 及更高版本中,新行为始终适用。

【讨论】:

  • 那么,StopIteration 被函数定义(或等效的生成器结构)消耗了吗?我只是想弄清楚如果我们在函数体之外使用 next 会引发异常,但如果我们在函数内部使用它会正常终止。
  • @BranAlgue 不,函数定义不会消耗异常。就像任何其他异常一样,StopIteration 将向上调用堆栈,直到它被显式 try/catch 块捕获,或者被 for 循环内的隐式块捕获。我认为您缺少的是 StopIteration 在生成器函数中不是问题。当你没有什么可以让步时,你应该筹集一个。您可以使用raise StopIteration() 显式执行此操作,或者通过到达函数末尾来隐式执行此操作——或者您可以让通过调用next 生成的StopIteration 未被捕获。
  • 我明白这一点。我不明白为什么 StopIteration 在生成器函数中不是问题。生成器隐式处理异常的说法正确吗?
  • @BranAlgue:这不是问题,因为StopIteration 是生成器用来表明它已完成的信号。如果您在生成器函数中并且使用next 手动迭代迭代器,则通常会在迭代器耗尽时完成。因此,与其提出自己的StopIteration 异常,不如让next 提出的异常冒泡。有一些反例,你想产生一个最终值,或者你需要在结束前做一些特殊的清理,在这些情况下你需要抓住StopIteration。但这并不常见。
【解决方案2】:

当一个函数包含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)

当然,真正的生成器很少如此微不足道。 :-)

【讨论】:

    【解决方案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
    

    【讨论】:

      【解决方案4】:

      yield 没有捕捉到StopIterationyield 为您的函数所做的是它使其成为生成器函数而不是常规函数。因此,从函数调用返回的对象是一个可迭代对象(当您使用 next 函数(由 for 循环隐式调用该函数)请求它时,它会计算下一个值)。如果您将 yield 语句排除在外,那么 python 会立即执行整个 while 循环,最终耗尽可迭代对象(如果它是有限的)并在您调用它时立即提升 StopIteration

      考虑:

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

      for 循环捕获异常——这就是它知道何时停止对您提供的可迭代对象调用 next 的方式。

      【讨论】:

        【解决方案5】:

        在 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]
        ............................
        

        【讨论】:

          猜你喜欢
          • 2021-01-25
          • 1970-01-01
          • 2023-01-25
          • 2023-04-09
          • 2017-03-14
          • 2016-12-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多