Skip to content

Latest commit

 

History

History
102 lines (84 loc) · 4.94 KB

异常 - 原力的黑暗面.md

File metadata and controls

102 lines (84 loc) · 4.94 KB

原文:Exceptions - The Dark Side of the Force


最近一篇博文"如果你不喜欢异常,那么你并不喜欢Python" 来回出现,并迫使我写一个部分反驳。并不是说那篇博文完全错误,只是它并不是这个主题的要义。而如果要我补充的话,它有点武断。

原文声称,异常是Python的核心,“异常应该只用于错误,而不是正常的流程控制”的常见建议是错误的,接着解释说,异常被用于核心实现,例如迭代器协议,以及属性访问,因此,它们是该语言的核心特征。博文的一些较长部分是关于揭穿Java和C++程序员的常见误解。

粗略来讲,那篇文章中的异常被描述成非常有用的,并且极具盛赞,以至于所有关于它们的使用的批评和问题都黯然失色。

使用异常来处理错误

这是我打心里同意barnert的一点。错误应该使用异常来传播,所以

def min(*lst):
    if not lst:
       raise ValueError("min(..) requires a list with at least one element")
    minimum = lst[0]
    for item in lst:
        if minimum > item:
             minimum = item
    return item

是异常的一个完全正确的使用,如果调用者的代码不能保证参数是长度大于0的列表,那么它必须检查这些异常。

异常与值和变量是分离的

有时,我被使用像这样的模式的代码呛到:

result = []
result.append(dosomething(bar))
try:
   foo = bar[key][anotherkey]
   res = dosomething(foo)
   result.append(res[evenanotherkey])
except KeyError:
   ....
finally:
   return result

这段代码有许多异常相关的问题,并且展示了如何不使用异常。首先,并不清楚try块中那一个键访问会引发异常。它可能是bar[key], 或者是_[anotherkey], 也有可能是res[evenanotherkey], 或者最后可能发生在dosomething(foo)。该异常机制将错误处理与值和变量分离。我的问题是:你能辨别出是否是想要从dosomething()中捕获KeyErrors吗?

因此,在使用异常时,对于捕获哪些异常以及不捕获哪些异常,人们必须非常小心。 使用防御性编程式(例如,haskey())的检查,它是明确的,并几乎与为每一个索引操作写出单独的try-catch块一样对代码是“侵入性的”。

异常风险

所以,使用异常时,基本上有两种风险:

  1. 应该捕获的异常没捕获
  2. 错误的捕获异常

第一个风险当然是一个风险,但对它,我并不过于担心。第二个是我非常害怕的一个风险。你的代码中有多少函数能够抛出eyErrors, ValueError, IndexError, TypeError, 和 RuntimeError 异常呢?

作为Python化的goto的异常

异常可以模拟goto语句。当然,它们不仅跳转到堆栈的顶级,而且也跳转进语句中。在C代码中,goto是函数局部控制流和错误处理(对错误处理而言,他们更无争议)的一个主要工具:

int
max_in_two_dim(double * array, size_t N, size_t M, double *out) {
  if (N * M == 0)
     goto empty_array_lbl;
  double max = array[0];
  for (int i=0; i < N; ++i) {
      for (int j=0; j < M; ++j) {
        double val = array[j * N +k];
        if (val != val) // NaN case
           goto err_lbl;
        if (max < val)
           max = val;
      }
  }
  return 0;
  nan_lbl:
    fprintf(stderr, "encountered a not-a-number value when unexpected");
    return -1;
  empty_array_lbl:
    fprintf(stderr, "no data in array with given dims");
    return -2;
}

你可以使用Python中的异常来模拟这种用法。我已经见过大量这种代码:

def whatever(arg1, arg2):
  try:
      for i in range(N):
          for j in range(M):
            # ..
            if ...:
               raise RuntimeError("jump")
      return out
  except RuntimeError:
    # cleanup
    # ..

在大多数情况下,有更好的办法来避免这种模式。Python的for循环有一个可选的else分支,用来帮助避免这种跳转。然而,在循环等其他一些地方上,这种模式会出现RuntimeError错误。

Meta: 局内人与局外人的思考

我最不喜欢barnert的文章的地方几乎可能是可以才能够标题:“如果……,那么你并不喜欢Python”中读到的东西。这与我所听到的很多关于代码/软件/解决方法是“Pythonic”的并驾齐驱。它似乎暗示着必须站队:你是与正统的Python社区站在一边呢,还是一个局外人,即那些不够“Pythonic”的人。所有这些都对改善代码毫无帮助。

总结

异常时Python中一个中心且强大的工具。但请小心谨慎的使用它们。不要假装它们像一个魔术棒,也不要为了显示你对Python的爱来使用它们。在要求异常使用的单一情景下使用它们。