原文: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
块一样对代码是“侵入性的”。
所以,使用异常时,基本上有两种风险:
- 应该捕获的异常没捕获
- 错误的捕获异常
第一个风险当然是一个风险,但对它,我并不过于担心。第二个是我非常害怕的一个风险。你的代码中有多少函数能够抛出eyErrors, ValueError, IndexError, TypeError, 和 RuntimeError 异常呢?
异常可以模拟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
错误。
我最不喜欢barnert的文章的地方几乎可能是可以才能够标题:“如果……,那么你并不喜欢Python”中读到的东西。这与我所听到的很多关于代码/软件/解决方法是“Pythonic”的并驾齐驱。它似乎暗示着必须站队:你是与正统的Python社区站在一边呢,还是一个局外人,即那些不够“Pythonic”的人。所有这些都对改善代码毫无帮助。
异常时Python中一个中心且强大的工具。但请小心谨慎的使用它们。不要假装它们像一个魔术棒,也不要为了显示你对Python的爱来使用它们。在要求异常使用的单一情景下使用它们。