如何防止或捕获 yield 调用函数中的 StopIteration 异常?

How can I prevent or trap StopIteration exception in the yield-calling function?

我们的一个库中的生成器返回函数(即其中包含 yield 语句的函数)由于未处理的 StopIteration 异常而未通过某些测试。为方便起见,在 post 中我将此函数称为 buggy.

我一直没能找到buggy的方法来防止异常(不影响函数的正常运行)。同样,我还没有找到在 buggy.

中捕获异常(使用 try/except)的方法

(Client code using buggy 可以捕获这个异常,但是这发生得太晚了,因为具有正确处理导致条件的必要信息的代码此异常是 buggy 函数。)

我正在使用的实际代码和测试用例太复杂了 post 这里,所以我创建了一个非常简单,但也 非常人为 的玩具说明问题的示例。

首先,具有buggy功能的模块:

# mymod.py

import csv  # essential!

def buggy(csvfile):
    with open(csvfile) as stream:

        reader = csv.reader(stream)

        # how to test *here* if either stream is at its end?

        for row in reader:
            yield row

如评论所示,使用 csv 模块(来自 Python 3.x 标准库)是这个问题的一个基本特征1 .

该示例的下一个文件是一个脚本,旨在代表“客户端代码”。换句话说,这个脚本在这个例子之外的“真正目的”在很大程度上是无关紧要的。它在示例中的作用是提供一种简单、可靠的方法来引出 buggy 函数的问题。 (例如,它的一些代码可以重新用于测试套件中的测试用例。)

#!/usr/bin/env python3

# myscript.py

import sys
import mymod

def print_row(row):
    print(*row, sep='\t')

def main(csvfile, mode=None):
    if mode == 'first':
        print_row(next(mymod.buggy(csvfile)))
    else:
        for row in mymod.buggy(csvfile):
            print_row(row)

if __name__ == '__main__':
    main(*sys.argv[1:])

该脚本将 CSV 文件的路径作为强制参数和可选的第二个参数。如果省略第二个参数,或者它不是字符串 "first",脚本将打印到 stdout CSV 文件中的信息,但在 TSV 格式。如果第二个参数是字符串"first",那么只会打印第一行的信息。

当使用空文件和字符串 "first" 作为参数调用 myscript.py 脚本时,我试图捕获的 StopIteration 异常出现 2.

下面是这个代码的一个例子:

% cat ok_input.csv
1,2,3
4,5,6
7,8,9
% ./myscript.py ok_input.csv
1   2   3
4   5   6
7   8   9
% ./myscript.py ok_input.csv first
1   2   3
% cat empty_input.csv
# no output (of course)
% ./myscript.py empty_input.csv
# no output (as desired)
% ./myscript.py empty_input.csv first
Traceback (most recent call last):
  File "./myscript.py", line 19, in <module>
    main(*sys.argv[1:])
  File "./myscript.py", line 13, in main
    print_row(next(mymod.buggy(csvfile)))
StopIteration

问: 如何在 buggy 函数的词法范围内防止或捕获此 StopIteration 异常?


重要提示: 请记住,在上面给出的示例中,myscript.py 脚本是“客户端代码”的替代,因此在外部我们的控制。这意味着任何需要更改 myscript.py 脚本的方法都无法解决实际的现实问题,因此这不是该问题的可接受答案。

上面显示的简单示例与我们的实际情况之间的一个重要区别是,在我们的例子中,有问题的输入流不是来自空文件。问题出现在buggy(或者更确切地说,它的现实世界对应物)“过早”到达此流的末尾,可以这么说。

我认为如果我可以测试 stream 是否在其末尾,在 for row in reader: 行之前就足够了,但我也没有想出办法来做到这一点。测试 stream.read(1) 返回的值是 0 还是 1 会告诉我流是否在其末尾,但在后一种情况下 stream 的内部指针将指向一个字节太远 csvfile的内容。 (此时 stream.seek(-1, 1)stream.tell() 都不起作用。)


最后,对于任何想要 post 这个问题的答案的人:如果您利用我上面提供的示例代码在 [=124] 之前测试您的提案,那将是最有效的=]正在处理它。


编辑: 我试过的 mymod.py 的一种变体是这样的:

import csv  # essential!

def buggy(csvfile):
    with open(csvfile) as stream:

        reader = csv.reader(stream)

        try:
            firstrow = next(reader)
        except StopIteration:
            firstrow = None

        if firstrow != None:
            yield firstrow

        for row in reader:
            yield row

此变体失败并显示与原始版本几乎相同的错误消息。

当我第一次阅读@mcernak 的提案时,我认为它与上面的变体非常相似,因此预计它也会失败。然后我惊喜的发现不是这样的!因此,截至目前,有一个确定的候选人可以获得赏金。也就是说,我很想了解为什么上面的变体无法捕获异常,而@mcernak 却成功了。


1 我正在处理的实际案例是遗留代码;从 csv 模块切换到某个替代模块在短期内对我们来说不是一个选择。

2 请完全忽略这个演示脚本在使用空文件和字符串 "first" 作为参数。在此 post 的演示中引发 StopIteration 异常的特定输入组合并不代表导致我们的代码发出有问题的 StopIteration 异常的真实情况。因此,演示脚本对空文件加上 "first" 字符串组合的“正确响应”(无论可能是什么)与我正在处理的实际问题无关。

你可以用这种方式在 buggy 函数的词法范围内捕获 StopIteration 异常:

import csv  # essential!

def buggy(csvfile):
    with open(csvfile) as stream:

        reader = csv.reader(stream)

        try:
            yield next(reader)
        except StopIteration:
            yield 'dummy value'

        for row in reader:
            yield row

您基本上是从 reader 迭代器和

手动请求第一个值
  • 如果成功,则从 csv 文件中读取第一行并将其交给 buggy 函数的调用者
  • 如果失败,如空 csv 文件的情况,一些字符串,例如生成 dummy value 是为了防止 buggy 函数的调用者崩溃

之后,如果 csv 文件不为空,则将在 for 循环中读取(并产生)剩余的行。


编辑: 为了说明为什么问题中提到的 mymod.py 的其他变体不起作用,我在其中添加了一些打印语句:

import csv  # essential!

def buggy(csvfile):
    with open(csvfile) as stream:

        reader = csv.reader(stream)

        try:
            print('reading first row')
            firstrow = next(reader)
        except StopIteration:
            print('no first row exists')
            firstrow = None

        if firstrow != None:
            print('yielding first row: ' + firstrow)
            yield firstrow

        for row in reader:
            print('yielding next row: ' + row)
            yield row

        print('exiting function open')

运行 它给出以下输出:

% ./myscript.py empty_input.csv first
reading first row
no first row exists
exiting function open
Traceback (most recent call last):
  File "myscript.py", line 15, in <module>
    main(*sys.argv[1:])
  File "myscript.py", line 9, in main
    print_row(next(mymod.buggy(csvfile)))

这表明,在输入文件为空的情况下,第一个 try..except 块正确处理了 StopIteration 异常并且 buggy 函数继续正常运行。
buggy 的调用者在这种情况下得到的异常是由于 buggy 函数在完成之前没有产生任何值。

mcernak很好的解决和描述了你遇到的问题

但是,这个问题背后存在一个设计问题:调用者 有时 并不期望生成器,而是 non-empty 迭代器

换个角度来看,文件丢失了怎么办?对于来自 open 和 return 的函数句柄 IOError 一些哨兵或将其提升给调用者更有意义吗?

与其试图强迫您的生成器与虐待它的调用者一起工作,不如考虑

  • 提供两个函数(一个可以调用另一个)
  • 为生成器的最大行数提供参数(可能是最好的)
# mymod.py

import csv
import itertools
def notbuggy(csvfile, max_rows=None):
    with open(csvfile) as stream:
        yield from itertools.islice(csv.reader(stream), max_rows)
#!/usr/bin/env python3
# myscript.py

import sys
import mymod

def print_row(row):
    print(*row, sep='\t')

def main(csvfile, mode=None):
    max_rows = 1 if mode == "first" else None
    for row in mymod.notbuggy(csvfile, max_rows):
        print_row(row)

if __name__ == '__main__':
    main(*sys.argv[1:])


使用next()时,调用逻辑必须同意

之一
  • 永远不要在一个空的可迭代对象上调用它(先检查文件?)
  • 处理生成器的异常(StopIteration,一些自定义 Exception
  • 处理一些空标记(可能是 ""、一些字符串、Noneobject..)

然而,调用者做了 none 个,所以保证没有很好地设置!

如果调用者想要不止一行或将空标记解释为一个值怎么办?除非这些在文档中以某种方式传达,否则调用者总是会误用函数并且不知道为什么它会出现意外行为。

>>> next(iter(()))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> g = iter((1,))
>>> next(g)
1
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> print_row("STOP SENTINEL")
S   T   O   P       S   E   N   T   I   N   E   L