【问题标题】:How can I prevent or trap StopIteration exception in the yield-calling function?如何在 yield 调用函数中防止或捕获 StopIteration 异常?
【发布时间】:2021-01-25 13:11:57
【问题描述】:

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

我一直没能找到buggy防止异常的方法(不影响函数的正常运行)。同样,我也没有找到在buggy 内捕获异常(带有try/except)的方法。

(客户端代码使用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

正如评论所指出的,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",那么只会打印第一行的信息。

myscript.py 脚本被一个空文件和字符串"first" 作为参数调用时,我试图捕获的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(或者更确切地说,它的真实世界对应物)“过早”到达此流的末尾,可以这么说,就会出现问题。

我认为如果我可以在for row in reader: 行之前测试stream 是否在其末尾就足够了,但我也没有想出办法做到这一点。测试stream.read(1) 返回的值是 0 还是 1 将告诉我流是否在其末尾,但在后一种情况下,stream 的内部指针将指向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

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

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


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

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

【问题讨论】:

  • 我不知道你的真实代码是什么样子的,但这个玩具例子只是说明你的main 函数有问题。 buggy 函数非常好。
  • 如果你想让buggy 做一些 other 非常好的事情,不同于现在正在做的事情,那么说什么,也许我们可以帮忙(或告诉你这是不可能的)。
  • 您仍然忽略了您希望buggy 的行为方式的问题。客户端代码要求一些根本不存在的东西。 buggy 应该做什么?
  • 编辑中的变化没有意义。您的行为就像客户端从 csv 阅读器获得的 StopIteration 一样,而不是来自buggy 本身的信号,通知客户端没有更多内容了。是 客户端 违反了 Python 中的这一基本迭代契约(不处理此信号),而不是 buggy
  • 这绝对看起来像XY problem。试图捕获 StopIteration 似乎不是解决您最初试图解决的问题的正确方法。

标签: python python-3.x csv exception generator


【解决方案1】:

您可以通过这种方式在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 函数在完成之前没有产生任何值。

【讨论】:

  • 感谢您发布此信息。到目前为止,您的答案是获得赏金的明确候选人。不过,我发现您的提议非常令人费解,原因是我在刚刚添加到我的帖子的编辑中说明了原因。
  • @kjo 我已经解释了为什么您添加到帖子中的代码变体不适用于我的回答
  • 如果没有行,这当然 always 会产生虚拟值,不仅在客户端的if mode == 'first': 分支的情况下,而且在它的else: 分支的情况下可能是不可取的。或者他们也想在那里。谁知道......好吧,OP可能会,但拒绝告诉......
  • 只是对 EDIT 部分的建议:在 buggy 的末尾添加明确的 return 'everything went fine in buggy' 并在输出中包含 StopIteration 行。
  • 啊,你编辑的最后一行是关键!这就是我所缺少的。这解释了为什么我试图捕获异常的一切都失败了。现在我终于明白了。谢谢!
【解决方案2】:

mcernak很好地解决并描述了您遇到的问题

然而,这个问题背后存在一个设计问题:调用者有时期待的不是生成器,而是非空的迭代器

从另一个方面来看,如果文件丢失了怎么办?对于来自open 的函数句柄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..)

但是,调用者没有做这些,所以保证没有设置好!

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

>>> 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

【讨论】:

    猜你喜欢
    • 2013-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-28
    • 2017-09-30
    • 1970-01-01
    相关资源
    最近更新 更多