什么是 Python 中冒泡错误条件的惯用方式?

6

我一直在开发一个Python项目,它已经变得相当庞大,并且有几层函数。由于一些愚蠢的初期决策,我发现我必须修复很多崩溃问题,因为较低层的函数返回了高级函数中未预期的类型(通常是None)。

在我进行清理之前,我想知道指示错误条件并在更高级别的函数中处理它们的最pythonic的方法是什么?

对于大部分情况下,我所做的是,如果一个函数无法完成并返回其预期结果,我将返回None。这会变得有点麻烦,因为你最终不得不始终检查所有调用它的函数是否为None。

def lowLevel():
    ## some error occurred
    return None

    ## processing was good, return normal result string
    return resultString

def highLevel():
    resultFromLow = lowLevel()
    if not resultFromLow:
        return None

    ## some processing error occurred
    return None

    ## processing was good, return normal result string
    return resultString

我想另一个解决方案可能是抛出异常。这样,调用函数中仍然会有很多代码来处理异常。

似乎没有特别优雅的解决方案。其他人使用什么?在obj-c中,常见的模式是通过引用返回错误参数,然后调用方进行检查。

3个回答

5

这取决于你对无法完成处理的情况想要做什么。如果这些异常是普通控制流之外的,你需要清理、退出、发送电子邮件等等,那么抛出异常是你想要的。在这种情况下,处理代码只是必要的工作,基本上是不可避免的,尽管你可以重新抛出堆栈并在一个地方处理错误以保持整洁。

如果可以容忍这些错误,那么一种优雅的解决方案是使用Null Object模式。而不是返回None,你返回一个对象,它模仿了通常返回的真实对象的接口,但什么都不做。这使得所有下游代码都可以继续操作,毫不知情地绕过了失败的事实。这种模式的主要缺点是可能很难发现真正的错误,因为你的代码不会崩溃,但在运行结束时可能不会产生任何有用的东西。

Python中Null Object模式的常见例子是在低级函数返回空列表或字典时返回。使用此返回值并迭代元素的任何后续函数将静默地继续执行,无需进行错误检查。当然,如果你需要这个列表至少有一个元素,那么这种方法就行不通了,你又回到了处理异常情况的地步。


4

好消息是,你已经发现了使用返回值表示错误条件的问题所在。

异常是处理问题的pythonic方式。你需要回答的问题(我猜你已经回答了)是:有没有可以由low_level函数返回的有用默认值?如果有,那就使用它并运行;否则,引发异常(`ValueError'、'TypeError'或者是自定义错误)。

然后,在更高层次的调用堆栈中,在你知道如何处理该问题的地方,捕获异常并处理它。你不必立即捕获异常——如果high_level调用mid-level调用low_level,在mid_level中没有任何try/except也是可以的,让high_level来处理它。也许你只能在程序的顶部拥有一个try/except来捕获和记录所有未捕获和未处理的错误,这也是可以的。


3

虽然这并不一定是严格符合Python风格,但经验告诉我应该让异常“原地躺下”。

也就是说,不要不必要地隐藏它们或重新引发一个不同的异常。

有时候,让调用方失败而不是试图捕获和隐藏各种错误条件是一个好习惯。

显然,这个话题可能有点主观;但是,如果你不隐藏或引发不同的异常,那么调试代码会更容易,调用你的函数或api的人也更容易理解出了什么问题。

注意: 这个答案不完整--请参见评论。这个Q&A中提出的一些或全部答案可能应该以清晰简洁的方式组合在一起,呈现出各种问题和解决方案。


1
这样做忽略了重新引发异常的主要目的——向调用者呈现统一的异常API。例如,如果我在我的代码中使用HTTP POST实现Web服务提交,那么该提交操作可能会引发urllib2.HTTPError(服务器端处理错误)、urllib2.URLError(URL错误、终端点关闭)、ValueError(URL无法解析或缺失)、httplibInvalidURL(URL可解析但无效),在极少数情况下,还可能出现其他错误。如果我不处理这些错误,那么我的调用者就必须处理它们。很丑陋!相反,我捕获它们所有,引发ConnectionError,并根据需要添加上下文字符串。 - ire_and_curses
1
就让它们失败而言,这只有在您可以容忍运行时错误结束进程的情况下才有效。这在测试中可能很方便,但对于大多数生产软件来说,这并不是您想要的结果。 - ire_and_curses
这些是非常好的观点,也很有道理。显然,一个合适且可接受的答案需要花费超过2分钟的回答 :) 我觉得这个问题应该在之前就被回答和提出了 :/ - James Mills
1
@ire_and_curses: James说:“不要不必要地隐藏它们或者...”当然,改变原始异常有很好的理由,但也有很多不好的理由这样做。此外,“让它们失败”并不意味着让整个程序崩溃;如果low_level引发异常,则它已经失败了。您仍然可以在调用堆栈的更高位置捕获它并进一步处理它。 - Ethan Furman
1
@ire_and_curses:好的,虽然我认为你的回答更多地涵盖了库的观点,而James则是应用程序的观点。 - Ethan Furman
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接