复杂函数的Pythonic错误处理

6 投票
2 回答
816 浏览
提问于 2025-04-16 15:02

我想知道有没有一种更符合Python风格的方法来处理那些可能会出错的长时间运行的函数,这些错误并不会影响函数继续执行的能力。

举个例子,假设有一个函数,它接收一个网址列表,然后递归地获取这些网址及其链接资源。它会把获取到的资源存储在本地文件系统中,目录结构和网址结构相对应。简单来说,这就像是一个基础版的wget,用于下载一系列网页。

这个函数可能会在很多地方出错:

  • 某个网址可能无效,或者无法解析
  • 主机可能无法访问(可能是暂时的)
  • 保存到本地时可能会出现磁盘错误
  • 还有其他你能想到的情况。

如果在获取或保存某个资源时出错,只会影响到这个资源及其可能链接的子资源的处理,但函数仍然可以继续获取其他资源。

一种简单的错误处理模型是,当第一次出错时,抛出一个合适的异常,让调用者来处理。问题在于,这样会终止函数的执行,无法继续。虽然可以修复错误并从头开始,但这会导致之前的工作白费,而如果出现了永久性错误,可能就永远无法完成。

我想到的几种替代方案是:

  • 在发生错误时记录在一个列表中,停止处理该资源及其子资源,但继续处理下一个资源。如果错误太多,可以设定一个阈值来终止整个函数,或者干脆尝试处理所有资源。调用者可以在函数完成后查看这个列表,看看是否有问题。
  • 调用者可以提供一个可调用对象,每当发生错误时就调用它。这样就把记录错误的责任交给了调用者。甚至可以规定,如果这个可调用对象返回False,就停止处理。这将把阈值管理的责任转移给调用者。
  • 将前一种方法和后一种方法结合起来,提供一个错误处理对象,来实现前者的行为。

在Python的讨论中,我常常注意到某些方法被称为Pythonic或非Pythonic。我想知道有没有特别符合Python风格的方法来处理上述场景。

Python是否有一些内置的工具,可以实现比简单的异常处理更复杂的错误处理,还是说更复杂的内置工具使用的错误处理模型是我应该借鉴的,以保持Pythonic的风格?

注意:请不要专注于这个例子。我并不是想解决这个特定领域的问题,但这个例子似乎是大多数人都能理解的一个好例子。

2 个回答

0

错误处理应该依赖于异常和日志记录,所以每当出现错误时,就抛出一个异常并记录一个错误信息。

然后在任何调用这个函数的地方捕获这个异常,如果需要的话再记录其他的错误信息,并处理这个问题。

如果这个问题没有完全处理好,那么可以再次抛出这个异常,这样上层的代码就可以捕获到同样的异常,并采取不同的措施。

在这些过程中,你可以保持一个计数器,记录某些类型的异常,这样只有在出现特定数量的问题时,才会执行某些操作。

6

我觉得在你提到的这个层面上,"Pythonic"和"非Pythonic"之间并没有特别清晰的区别。

没有一个通用的解决方案,主要是因为你想要的具体语义会根据不同的问题而有所不同。

  • 在某些情况下,遇到第一个错误就停止可能就足够了。
  • 在另一些情况下,如果任何操作失败,你可能想要停止并回滚。
  • 还有一种情况,你可能希望尽量完成所有操作,然后记录并忽略失败。
  • 再有一种情况,你可能希望尽量完成所有操作,但在最后抛出一个异常来报告失败的情况。

即使支持错误处理程序,也不一定能覆盖所有这些期望的行为——一个简单的每个失败都有处理的错误处理程序,无法轻松提供停止并回滚的语义,或者在最后生成一个异常。(这并不是不可能的——你只需要通过一些技巧,比如传递绑定的方法或闭包作为错误处理程序,来实现)

所以你能做的就是根据典型的使用场景和在错误发生时希望的行为,做出合理的猜测,并据此设计你的API。

一个完全通用的解决方案应该接受一个错误处理程序,在每次发生错误时提供给它,并有一个最终的“发生错误”处理程序,让调用者决定如何处理多个错误(并有一些协议允许将数据从单个错误处理程序传递到最终的批量错误处理程序)。

然而,提供这样一个通用的解决方案可能会导致API设计失败。API的设计者不应该害怕对他们的API应该如何使用、错误应该如何处理发表看法。最重要的是要记住,不要过度设计你的解决方案:

  • 如果简单的方法就足够了,就不要去改动它。
  • 如果把失败收集到一个列表中并报告一个错误就足够了,那就这样做。
  • 如果一部分失败就需要回滚所有内容,那就直接这样实现。
  • 如果确实有自定义错误处理的需求,那就把错误处理程序作为API的一部分。但在这样做时,要有明确的使用场景,而不是为了做而做。当你这样做时,要有一个合理的默认处理程序,如果用户没有指定,就使用这个默认处理程序(这可能就是简单的“立即抛出”方法)。
  • 如果你提供可选择的错误处理程序,考虑提供一些标准的错误处理程序,可以作为可调用对象或命名字符串传入(比如类似于文本编码的错误处理程序选择)。

也许你能得到的最基本原则是,“Pythonic”的错误处理应该尽可能简单,但不能更简单。但在这个时候,这个词只是作为“好代码”的同义词使用,这并不是它的本意。

另一方面,谈论非Pythonic错误处理的实际形式会稍微容易一些:

def myFunction(an_arg, error_handler)
  # Do stuff
  if err_occurred:
    if isinstance(err, RuntimeError):
      error_handler.handleRuntimeError()
    elif  isinstance(err, IOError):
      error_handler.handleIOError()

Pythonic的做法是,如果支持错误处理程序,它们应该只是简单的可调用对象。给它们提供处理情况所需的信息,而不是过多地替它们做决定。如果你想让实现常见的错误处理方面更简单,那就提供一个单独的助手类,里面有一个__call__方法来处理,这样人们可以决定是否使用它(或者在使用时想覆盖多少内容)。这并不是完全Python特有的,但对于那些来自于让人烦恼的难以传递任意可调用对象的语言(如Java、C、C++)的人来说,可能会搞错。所以复杂的错误处理协议绝对是走向“非Pythonic错误处理”领域的一种方式。

上述非Pythonic代码的另一个问题是没有提供默认处理程序。强迫每个API用户做出他们可能还没有准备好的决定,这就是糟糕的API设计。但现在我们又回到了“好代码”和“坏代码”的一般领域,所以Pythonic和非Pythonic真的不应该用来描述这种区别。

撰写回答