Section 199.1: 过度宽泛的 except 子句

异常功能强大,但一个过度宽泛的 except 子句可能会在一行代码中将所有优势都抹杀。

try:
    res = get_result()
    res = res[0]
    log('got result: %r' % res)
except:
    if not res:
        res = ''
    print('got exception')

这个例子展示了反模式的三个症状:

  1. 不带异常类型的 except(第 5 行)会捕获所有异常,包括 KeyboardInterrupt。这在某些情况下会阻止程序退出。
  2. except 块没有重新抛出错误,这意味着我们无法判断异常是来自 get_result 内部,还是因为 res 是一个空列表。
  3. 最糟糕的是,如果我们担心结果为空,我们已经造成了更严重的问题。如果 get_result 失败,res 将完全未设置,而 except 块中对 res 的引用将引发 NameError,完全掩盖了原始错误。

始终考虑您要处理的异常类型。阅读异常页面,了解基本异常的存在。

以下是上述例子的修正版本:

import traceback
try:
    res = get_result()
except Exception:
    log_exception(traceback.format_exc())
    raise
try:
    res = res[0]
except IndexError:
    res = ''
log('got result: %r' % res)

我们捕获更具体的异常,并在必要时重新抛出。虽然多了一些行,但正确性提高了无数倍。

Section 199.2: 在调用计算密集型函数前先检查

程序很容易通过多次调用计算密集型函数而浪费时间。

例如,考虑这样一个函数:如果输入值可以产生一个整数,则返回一个整数,否则返回 None:

def intensive_f(value): # int -> Optional[int]
  # complex, and time-consuming code
  if process_has_failed:
    return None
  return integer_output

它可以这样使用:

x = 5
if intensive_f(x) is not None:
  print(intensive_f(x) / 2)
else:
  print(x, "could not be processed")

print(x)

虽然这可以工作,但它存在调用两次 intensive_f 的问题,这将使代码运行时间翻倍。一个更好的解决方案是提前获取函数的返回值。

x = 5
result = intensive_f(x)
if result is not None:
  print(result / 2)
else:
  print(x, "could not be processed")

然而,一个更清晰且可能更符合 Python 风格的方法是使用异常,例如:

x = 5
try:
  print(intensive_f(x) / 2)
except TypeError: # The exception raised if None + 1 is attempted
  print(x, "could not be processed")

这里不需要临时变量。在许多情况下,使用 assert 语句并捕获 AssertionError 可能会更好。

字典键

一个常见的例子是访问字典键。例如比较:

bird_speeds = get_very_long_dictionary()

if "european swallow" in bird_speeds:
  speed = bird_speeds["european swallow"]
else:
  speed = input("What is the air-speed velocity of an unladen swallow?")

print(speed)

与:

bird_speeds = get_very_long_dictionary()

try:
  speed = bird_speeds["european swallow"]
except KeyError:
  speed = input("What is the air-speed velocity of an unladen swallow?")

print(speed)

第一个例子需要两次遍历字典,由于这是一个很长的字典,每次遍历可能都需要很长时间。第二个例子只需要一次遍历字典,从而节省了大量的处理器时间。

另一种选择是使用 dict.get(key, default),但许多情况可能需要在键不存在时执行更复杂的操作。