【问题标题】:Pythonic error handling of complex functions复杂函数的 Pythonic 错误处理
【发布时间】:2011-04-04 06:53:53
【问题描述】:

我想知道是否有一种 Pythonic 方式来处理长时间运行的函数中的错误,这些错误可能会出现部分不影响函数继续能力的错误。

例如,考虑一个给定 URL 列表的函数,它递归地检索顶级 URL 路径下的资源和所有链接资源。它将检索到的资源存储在本地文件系统中,其目录结构反映了 URL 结构。本质上,这是一个页面列表的基本递归 wget。

这个函数在很多地方可能会失败:

  • 网址可能无效或无法解析
  • 主机可能无法访问(可能是暂时的)
  • 本地保存可能会出现磁盘错误
  • 你能想到的任何其他东西。

检索或保存任何一个资源失败只会影响函数继续处理该资源以及可能从该资源链接的任何子资源的能力,但可以继续检索其他资源。

错误处理的一个简单模型是,在第一个错误时,会引发适当的异常供调用者处理。这样做的问题是它终止了函数并且不允许它继续。错误可能会被修复,函数从头开始重新启动,但这会导致工作重做,任何永久性错误都可能意味着我们永远无法完成。

我想到的几个选择是:

  • 在列表中记录发生的错误并中止处理该资源的任何子资源,但继续处理下一个资源。如果发生太多错误,可以使用阈值来中止整个函数,或者可能只是尝试一切。调用者可以在函数完成时询问这个列表,看看是否有任何问题。
  • 调用者可以提供一个可调用对象,每个错误都会调用该对象。这将记录错误的责任转移回调用者。您甚至可以指定如果可调用返回 False 则应停止处理。这会将阈值管理转移给调用者。
  • 用后者实现前者,提供一个错误处理对象,而不是对前者的行为进行编码。

在 Python 讨论中,我经常注意到某些被描述为 Python 或非 Python 的方法。我想知道是否有任何特别 Pythonic 的方法来处理上述类型的场景。

Python 是否包含比异常处理终止模型更复杂的错误处理模型的电池,或者包含的更复杂的电池是否使用我应该复制以保持 Pythonic 的错误处理模型?

注意:请不要专注于示例。我不打算解决那个特定领域的问题,但这似乎是一个很好的例子,这里的大多数人都会理解。

【问题讨论】:

标签: error-handling python


【解决方案1】:

我认为在您在这里谈论的级别上没有特别明显的“Pythonic/non-Pythonic”区别。

在这个领域没有“一刀切”的解决方案的一个重要原因是,您想要的确切语义将是针对特定问题的。

  • 对于一种情况,第一次失败时中止可能就足够了。
  • 另一方面,如果任何操作失败,您可能需要中止和回滚。
  • 对于第三种情况,您可能希望尽可能多地完成并简单地记录并忽略失败
  • 对于第四种选择,您可能希望尽可能多地完成,但在最后引发异常以报告任何失败的情况。

即使支持错误处理程序也不一定涵盖所有这些所需的行为 - 简单的每次失败错误处理程序无法轻松提供中止和回滚语义,或在最后生成单个异常。 (这不是不可能的——你只需要弄乱一些技巧,比如传递绑定方法或闭包作为你的错误处理程序)

因此,您能做的最好的事情就是对典型的使用场景和面对错误时的理想行为进行有根据的猜测,并相应地设计您的 API。

一个完全通用的解决方案将接受一个 on-error 处理程序,该处理程序会在每次失败时给出它,以及一个最终的“错误发生”处理程序,让调用者有机会决定如何处理多个错误(使用一些协议允许数据要从单个错误处理程序传递到最终的批处理错误处理程序)。

但是,提供这样一个通用的解决方案很可能是 API 设计失败。 API 的设计者不应该害怕对如何使用他们的 API 以及如何处理错误有意见。要记住的主要事情是不要过度设计您的解决方案:

  • 如果天真的方法足够了,就不要乱用它
  • 如果在列表中收集故障并报告单个错误就足够了,那么就这样做
  • 如果您需要在某个部分发生故障时回滚所有内容,那么只需以这种方式实现它
  • 如果有自定义错误处理的真正用例,则接受错误处理程序作为 API 的一部分。但是,当你这样做时,请记住一个特定的用例,不要只是为了它而这样做。当你这样做时,有一个合理的 default 处理程序,如果用户没有指定一个处理程序(这可能只是天真的“立即提升”方法)
  • 如果您确实提供了可选的错误处理程序,请考虑提供一些标准错误处理程序,这些错误处理程序可以作为可调用对象或命名字符串传入(即按照文本编解码器的错误处理程序选择行)

作为一般原则,您可能会得到的最好的结果是“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++)的人可能会出错。如此复杂的错误处理协议肯定是进入“非 Python 错误处理”领域的一种方式。

上述非 Pythonic 代码中的另一个问题是没有提供默认处理程序。强迫每个 API 用户做出他们可能还没有能力做出的决定只是糟糕的 API 设计。但是现在我们又回到了“好代码”/“坏代码”领域,所以 Pythonic/non-Pythonic 真的不应该用来描述差异。

【讨论】:

  • 感谢您的详细解答。我将完全按照您建议的方法进行 - 天真的方法,直到有证据表明需要更多,如果需要,则使用默认处理程序的可插入可调用方法。随着我的 API 的客户端越来越多,我将更加了解客户端希望如何处理错误。
【解决方案2】:

错误处理应依赖异常和日志记录,因此对于每个错误都会引发异常并记录错误消息。

然后在任何调用者函数级别捕获异常,如果需要记录任何其他附加错误并处理问题。

如果问题没有得到完全处理,那么re-raise the exception again 以便上层可以捕获相同的异常并执行不同的操作。

在此阶段的任何一个阶段,您都可以对某些类型的异常进行计数,以便仅在出现特定数量的问题时才能执行某些操作。

【讨论】:

  • 这是我描述的错误处理的简单模型。由于我所说的原因,我正在寻找更灵活的东西。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-02-25
  • 1970-01-01
  • 2013-12-13
  • 2019-05-03
  • 1970-01-01
  • 2018-11-28
  • 1970-01-01
相关资源
最近更新 更多