尝试并捕获异常还是先测试是否可能避免异常更好?

153

我应该测试 if 语句来检查某个东西是否有效,还是直接尝试并捕获异常呢?

  • 有没有权威的文档说明哪种方式更好?
  • 哪种方式更符合Python风格?

例如,我应该:

if len(my_list) >= 4:
    x = my_list[3]
else:
    x = 'NO_ABC'

或者:

try:
    x = my_list[3]
except IndexError:
    x = 'NO_ABC'

一些想法...
PEP 20 讲道:

错误不应该悄无声息地传递。
除非是被明确地消除。

在某些情况下,使用 try 而不是 if 是否被视为错误的悄无声息传递?如果是这样,那么通过这种方式使用它是否是在明确消除错误的情况下进行了显式静音,从而使其变得可以接受?


不是指在只有一种方法可行的情况下;例如:

try:
    import foo
except ImportError:
    import baz
8个回答

172

如果可以通过使用try/except来实现以下效果:

  • 加快速度(例如,避免额外的查找)
  • 更干净的代码(更少的行数/更容易阅读)

通常这两者是相辅相成的。


加快速度

如果在长列表中查找元素,则应使用try/except语句:

try:
    x = my_list[index]
except IndexError:
    x = 'NO_ABC'

index 可能在列表中并且 IndexError 通常不会被抛出时,try except 是最好的选择。这样可以避免通过 if index < len(my_list) 进行额外的查找。

Python 鼓励使用异常处理,“你处理”的是一句来自 Dive Into Python 的话。你的例子不仅可以(优雅地)处理异常,而不是让它“默默地”通过。此外,异常只会在未找到索引的情况下发生(因此是“exceptional”情况!)。


更干净的代码

官方 Python 文档提到了EAFP 原则: 宁求原谅勿问许可Rob Knight 指出,捕获错误而非避免错误,可以导致更清晰、易于阅读的代码。他的例子描述如下:

更糟的做法 (LBYL '先看一眼再跳'):

#check whether int conversion will raise an error
if not isinstance(s, str) or not s.isdigit():
    return None
elif len(s) > 10:    #too many digits for int conversion
    return None
else:
    return int(s)

更好的 (EAFP: 宁可请求宽恕,不要事先征得许可):

try:
    return int(s)
except (TypeError, ValueError, OverflowError): #int conversion failed
    return None

if index in mylist 测试index是否是mylist的一个元素,而不是可能的索引。你应该使用 if index < len(mylist) 代替。 - ychaouche
3
这很有道理,但我在哪里可以找到明确说明int()可能抛出哪些异常的文档呢?https://docs.python.org/3/library/functions.html#int 我在这里找不到这个信息。 - BrutalSimplicity
相反地,您可以将此案例(来自官方文档)添加到您的答案中,以说明何时应优先选择if/else而不是try/catch - rappongy

22

在这种特定情况下,你应该完全使用其他东西:

x = myDict.get("ABC", "NO_ABC")

一般而言:如果你预计测试经常失败,请使用if。如果测试相对于尝试操作并在失败时捕获异常来说代价高昂,请使用try。如果这两种情况都不适用,则选择阅读更容易的方式。

2
+1 对于代码示例下的解释非常准确。 - ktdrv
4
我认为很清楚这不是他所询问的内容,而他现在已经编辑了帖子,使其更加明确。 - agf

10

如果存在竞态条件,直接使用tryexcept而不是在if守卫内部使用应该始终如一。例如,如果您想确保目录存在,请不要这样做:

import os, sys
if not os.path.isdir('foo'):
  try:
    os.mkdir('foo')
  except OSError, e
    print e
    sys.exit(1)

如果在 isdirmkdir 之间,另一个线程或进程创建了该目录,则您将退出。因此,请执行以下操作:
import os, sys, errno
try:
  os.mkdir('foo')
except OSError, e
  if e.errno != errno.EEXIST:
    print e
    sys.exit(1)

只有当无法创建“foo”目录时,才会退出。

8
如果在执行某个操作之前,检查某些内容是否会失败是微不足道的话,那么你应该更倾向于这样做。毕竟,构建异常(包括与之相关的回溯)需要时间。
异常应该用于以下情况:
1. 意外情况,或者... 2. 需要跳过多层逻辑(例如,break不能满足需求)的情况,或者... 3. 无法预先确定将处理异常的确切对象的情况,或者... 4. 相对于尝试操作而言,提前检查失败的代价很高。
请注意,通常真正的答案是“两者都不需要” - 例如,在您的第一个示例中,您真正应该做的就是使用.get()来提供默认值:
x = myDict.get('ABC', 'NO_ABC')

除了 if 'ABC' in myDict: x = myDict['ABC']; else: x = 'NO_ABC' 通常比使用 get 更快之外,不幸的是。我并不是说这是最重要的标准,但这是需要注意的事项。 - agf
@agf:最好编写清晰简洁的代码。如果以后需要进行优化,很容易回来重写它,但是http://c2.com/cgi/wiki?PrematureOptimization - Amber
我知道;我的观点是,即使存在特定情况的替代方案,if / elsetry / except也可以有其用处,因为它们具有不同的性能特征。 - agf
@agf,你知道get()方法在未来的版本中是否会得到改进,以至于它至少能像显式查找一样快吗?顺便说一句,当进行两次查找时(例如if 'ABC' in d: d['ABC']),try: d['ABC'] except KeyError:...不是最快的方法吗? - Remi
2
@Remi,.get()的缓慢部分是Python级别的属性查找和函数调用开销;在内置关键字上使用基本上直接进入C。我不认为它会很快变得更快。至于if vs.try,请阅读dịct.get()方法返回指针,其中包含一些性能信息。命中和未命中的比率很重要(如果键几乎总是存在,则try可能会更快),字典的大小也很重要。 - agf

7
作为其他帖子所提到的,这取决于情况。在使用try/except替代事先检查数据有效性时,尤其是在处理大型项目时,存在一些危险。
  • 在捕获异常之前,try块中的代码可能有机会造成各种破坏-如果您提前使用if语句进行检查,可以避免这种情况。
  • 如果您在try块中调用的代码引发常见的异常类型,例如TypeError或ValueError,则实际上可能无法捕获您预期捕获的相同异常-它可能是在甚至到达可能引发异常的行之前或之后引发相同的异常类的其他异常。
例如,假设您有以下内容:
try:
    x = my_list[index_list[3]]
except IndexError:
    x = 'NO_ABC'

IndexError并没有说明它是在尝试获取index_list或my_list的元素时发生的。

4
使用try相对于if来说,是承认错误可能会发生,这与让错误默默地发生恰恰相反。使用except则是完全防止错误发生。
在逻辑较为复杂的情况下,使用try: except:更为合适,而不是if: else:。简单胜于复杂,复杂胜于混乱;请求宽恕比获得许可容易得多。
“错误不应该默默通过”所警告的是,代码可能会引发异常,你知道这一点,并且你的设计允许这种可能性,但你没有以处理异常的方式进行设计。明确地消除错误,在我看来,就像在except块中使用pass一样,只有在你确定“什么都不做”确实是特定情况下正确的错误处理方式时才能这样做。(这是我觉得非常需要在编写良好的代码中添加注释的少数几次之一。)
然而,在你的特定示例中,两者都不合适:
x = myDict.get('ABC', 'NO_ABC')

大家指出这一点的原因是,尽管您承认自己希望理解并且无法想出更好的例子,但在很多情况下实际上存在等效的绕路方法,寻找它们是解决问题的第一步。


3
每当你使用try/except来控制流程时,请先思考以下几点:
  1. 是否轻松地看出try块何时成功或失败?
  2. 您是否知晓try块内的所有副作用?
  3. 您是否知晓try块抛出异常的所有情况?
  4. 如果try块的实现发生更改,您的控制流程是否仍会按预期运行?

如果以上问题的答案有一个或多个是“否”,那么您可能需要向将来的自己寻求谅解。


例如, 我最近在一个大型项目中看到的代码如下:

try:
    y = foo(x)
except ProgrammingError:
    y = bar(x)

与程序员交流后,得知原本的控制流程为:
如果x是整数,则执行y = foo(x)
如果x是整数列表,则执行y = bar(x)
这种实现方式可行是因为foo会进行数据库查询,并且如果x是一个整数,则查询将成功。如果x是一个列表,则会抛出ProgrammingError。
在此处使用try/except是不明智的:
1. 异常名ProgrammingError无法反映实际问题(即x不是整数),这使得很难理解发生了什么。
2. ProgrammingError是在数据库调用期间引发的,这浪费了时间。如果foo在抛出异常之前向数据库写入某些信息或者改变其他系统状态,情况将变得更糟糕。
3. 不清楚是否仅当x是整数列表时才会引发ProgrammingError。例如,假设foo的数据库查询中有拼写错误,这也可能引发ProgrammingError。这会导致在x是整数时也调用bar(x)。这可能导致产生难以理解的异常或者产生无法预见的结果。
4. try/except块增加了对foo未来所有实现的要求。每当我们更改foo时,我们必须考虑它如何处理列表并确保其引发ProgrammingError而不是AttributeError或没有任何错误。

0

如果想要了解一般含义,可以阅读Python 中的成语和反语:异常

对于你的特定情况,正如其他人所述,你应该使用dict.get()

get(key[, default])

如果字典中有 key,则返回与之对应的值;否则返回默认值。如果没有给出默认值,则默认为 None,因此此方法永远不会引发 KeyError 异常。


我认为该链接根本不涵盖这种情况。它的示例是关于处理代表真正错误的事情,而不是关于在预期情况下是否使用异常处理。 - agf
第一个链接已经过时。 - Timofei Bondarev

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