如何在 yield-calling 函数中防止或捕获 StopIteration 异常?

kjo*_*kjo 6 python csv exception generator python-3.x

yield由于未处理的StopIteration异常,我们库之一中的生成器返回函数(即其中包含语句的函数)在某些测试中失败。为方便起见,在这篇文章中,我将此函数称为buggy.

我一直没能找到buggy防止异常的方法(不影响函数的正常运行)。类似地,我还没有找到一种方法来陷阱异常(具有try/except内)buggy

(使用的客户端代码buggy可以捕获此异常,但这发生为时已晚,因为具有正确处理导致此异常的条件所需的信息的代码是buggy函数。)

我正在使用的实际代码和测试用例过于复杂,无法在此处发布,因此我创建了一个非常简单但也非常人为的玩具示例来说明问题。

一、具有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
Run Code Online (Sandbox Code Playgroud)

如评论所示,使用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:])
Run Code Online (Sandbox Code Playgroud)

该脚本将 CSV 文件的路径作为必需参数和可选的第二个参数。如果省略第二个参数,或者它不是 string "first",则脚本将打印到stdoutCSV 文件中的信息,但采用TSV格式。如果第二个参数是 string "first",则只会打印第一行中的信息。

StopIterationmyscript.py使用空文件和字符串"first"作为参数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
Run Code Online (Sandbox Code Playgroud)

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


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

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

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


最后,对于想要发布此问题答案的任何人:如果您在发布之前利用我上面提供的示例代码来测试您的提案,那将是最有效的。


编辑: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
Run Code Online (Sandbox Code Playgroud)

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

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


1我处理的实际情况是遗留代码;csv短期内从模块切换到其他替代方案对我们来说不是一个选择。

2请完全忽略这个演示脚本的“正确响应应该是什么”的问题,当它被一个空文件和字符串"first"作为参数调用时。StopIteration在这篇文章的演示中引发异常的特定输入组合并不代表导致我们的代码发出有问题的StopIteration异常的真实世界条件。因此,演示脚本对空文件加"first"字符串组合的“正确响应”,无论是什么,都与我正在处理的实际问题无关。

mce*_*nak 5

您可以通过这种方式StopIterationbuggy函数的词法范围内捕获异常:

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
Run Code Online (Sandbox Code Playgroud)

您基本上手动从reader迭代器请求第一个值,然后

  • 如果成功,则从 csv 文件中读取第一行并将其交给buggy函数的调用者
  • 如果失败,就像空 csv 文件的情况一样,dummy value会产生一些字符串 eg以防止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')
Run Code Online (Sandbox Code Playgroud)

运行它会给出以下输出:

% ./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)))
Run Code Online (Sandbox Code Playgroud)

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