Skip to content

检测和处理异常

在我们写代码的时候,有时候有些代码它可能会报错,有可能又不会报错。这时候我们希望能够运行,又希望它不会影响后续代码的执行,就可以用异常处理语句对其进行检测。

异常可以通过 try 语句来检测。任何在 try 语句块里的代码都会被监测,检查有无异常发生。

try 语句有两种主要形式:try-excepttry-finally 。这两个语句是互斥的,也就是说你只能使用其中的一种。一个 try 语句可以对应一个或多个 except 子句,但只能对应一个 finally 子句,或是一个 try-except-finally 复合语句。

try-except 语句

try-except 语句定义了进行异常监控的一段代码,并且提供了处理异常的机制。

最常见的 try-except 语句语法如下所示。它由 try 块和 except 块组成,也可以有一个可选的错误原因。

python
try:
    try_suite  # 监控这里的代码
except Exception as e:
    except_suite  # 异常处理代码

我们用一个例子说明这一切是如何工作的,下面这个案例一旦输入字符,就非常用以报错,把我们的代码封装在 try-except 里,让代码更健壮:

python
try:
    number = input('请输入一个数字')
    number = int(number)
except Exception as e:
    print(e)

print('程序执行完毕')

在输入正常数字字符时,代码没有问题,运行完 try 里面的代码然后顺利走完程序。如果输入一个英文字符,代码就会出现报错。报错信息在 try 里面触发,然后被 except 捕获,最后将错误信息 e 打印出来,程序再正常执行完毕。

上面的例子使用了 Exception 这个错误顶级类,会把所有的错误类型全部捕捉进去。但是引发程序的错误类型会有很多种,这个在 异常的类型 一节中有说明。这个有点类似 elseelif 的关系,推荐使用实际报错的错误类代替 Exception 进行捕捉。

带有多个 except 的 try 语句

前边,已经介绍了 except 的基本语法:

python
except Exception[, reason]:
suite_for_exception_Exception

这种格式的 except 语句指定检测名为 Exception 的异常。你可以把多个 except 语句连接在一起,处理一个 try 块中可能发生的多种异常,如下所示:

python
except Exception1[, reason1]:
suite_for_exception_Exception
except Exception2[, reason2]:
suite_for_exception_Exception

同样,首先尝试执行 try 子句,如果没有错误,忽略所有的 except 从句继续执行。如果发生异常,解释器将在这一串处理器(except 子句)中查找匹配的异常如果找到对应的处理器,执行流将跳转到这里。

好的代码能够处理好每一种异常。这就需要多个except语句,每个 except 语句对应一种异常类型。Python 支持把 except 语句串连使用我们将分别为每个异常类型分别创建对应的错误信息,用户可以得到更详细的关于错误的信息:

python
def str_2_float(str_):
    try:
        return float(str_)
    except ValueError:
        return '不能将一个 Nan 转化为浮点数'
    except TypeError:
        return '类型错误,请传入正确的内容'

使用错误的参数调用这个函数,我们得到下面的输出结果:

python
>>> def str_2_float(str_):
...     try:
...         return float(str_)
...     except ValueError:
...         return '不能将一个 Nan 转化为浮点数'
...     except TypeError:
...        return '类型错误,请传入正确的内容'
...
>>> str_2_float({'a': 'dict'})
'类型错误,请传入正确的内容'
>>> str_2_float('a')
'不能将一个 Nan 转化为浮点数'

处理多个异常的 except 语句

我们还可以在一个 except 子句里处理多个异常。except 语句在处理多个异常时要求异常被放在一个元组里:

python
except (Exception1, Exception2)[, reason]:
    suite_for_exception_Exception

上边的语法展示了如何处理同时处理两个异常。事实上 except 语句可以处理任意多个异常,前提只是它们被放入一个元组里,如下所示:

python
except (Exce[, Exce[, ...]])[, reason]:
    suite_for_exception_Exception

如果由于其他原因,也许是内存规定或是设计方面的因素,要求 str_2_float() 函数中的所有异常必须使用同样的代码处理,那么我们可以这样满足需求:

python
def str_2_float(str_):
    try:
        return float(str_)
    except (ValueError, TypeError):
        return '参数必须是一个数字或者是一个字符串数字'

现在,错误的输入会返回相同的字符串

捕获所有异常

使用前一节的代码,我们可以捕获任意数目的指定异常,然后处理它们。如果我们想要捕获所有的异常呢?当然可以! 自版本 1.5 后,异常成为类,实现这个功能的代码有了很大的改进。也因为这点(异常成为类),我们现在有一个异常继承结构可以遵循。

如果查询异常继承的树结构,我们会发现 Exception 是在最顶层的,所以我们的代码可能看起来会是这样:

python
try:
    pass
except Exception as e:
    pass

我们没有指定任何要捕获的异常——这不会给我们任何关于可能发生的错误的信息。另外它会捕获所有异常,你可能会忽略掉重要的错误,正常情况下这些错误应该让调用者知道并做一定处理。最后,我们没有机会保存异常发生的原因。当然,你可以通过 sys.exc_info() 获得它,但这样你就不得不去导入sys模块,然后执行函数——这样的操作本来是可以避免的,尤其当我们需要立即告诉用户为什么发生异常的时候。在 Python 的未来版本中很可能不再支持空 except 子句(参见“核心风格”)。

很明显,错误无法避免,try-except 的作用是提供一个可以提示错误或处理错误的机制,而不是一个错误过滤器。上边这样的结构会忽略许多错误,这样的用法是缺乏工程实践的表现,我们不赞同这样做。

底线:避免把大片的代码装入 try-except 中然后使用 pass 忽略掉错误。你可以捕获特定的异常并忽略它们,或是捕获所有异常并采取特定的动作。不要捕获所有异常,然后忽略掉它们。

finally 子句

finally 子句是无论异常是否发生,是否捕捉都会执行的一段代码。你可以将 finally 仅仅配合 try 一起使用,也可以和 try-except (else 也是可选的)一起使用。独立的 try-finally 将会在下一章介绍,我们稍后再来研究。

下面是 try-except-finally 语法的示例:

python
try:
    A
except Exception as e:
    B
finally:
    C

finally 都是可选的。A、B、C是程序(代码块)。程序会按预期的顺序执行。(注意:可能的顺序是AD[正常]或AD[异常] )。无论异常发生在Α、Β和/或C都将执行finally块。旧式写法依然有效,所以没有向后兼容的问题。

我们暂时只是指出这个缺点,在进一步改进程序之前,首先来看看 try-except 的其他灵活的语法,特别是 except 语句,它有好几种变化形式。

完整格式

try-except-else-finally

我们综合了这一章目前我们所见过的所有不同的可以处理异常的语法样式:

python
try:
    pass  # 尝试做
except ValueError as e:
    pass  # 捕获一个错误
except (TypeError, SyntaxError) as e:
    pass  # 捕获多个错误
except Exception as e:
    pass  # 捕获其他错误
finally:
    pass  # 最终都会执行

回顾上面,finally 子句和 try-except 联合使用。这一节最重要的是无论你选择什么语法,你至少要有一个 except 子句,而 finally 都是可选的。

异常的传递性

我们的目标是“安全地”调用 str_2_float() 函数,或是使用一个“安全的方式”忽略掉错误,因为它们与我们转换数值类型的目标没有任何联系,而且这些错误也没有严重到要让解释器终止执行。 为了实现我们的目的,这里我们创建了一个“封装”函数,在 try-except 的协助下创建我们预想的环境,我们把他叫做safe_float() 。在第一次改进中我们搜索并忽略ValueError,因为这是最常发生的。而TypeError并不常见,我们一般不会把非字符串数据传递给float()。

python
def str_2_float(str_):
    try:
        return float(str_)
    except Exception as e:
        print(e)

我们采取的第一步只是“止血”。在上面的例子中,我们把错误“吞了下去”。换句话说,错误会被探测到,而我们在except从句里只是打印的错误信息,不进行任何处理,忽略这个错误。

这个解决方法有一个明显的不足,它在出现错误的时候没有明确地返回任何信息。虽然返回了None(当函数没有显式地返回一个值时,例如没有执行到 return object 语句函数就结束了,它就返回 None ,我们并没有得到任何关于出错信息的提示。我们至少应该显式地返回 None ,来使代码更容易理解:

python
def str_2_float(str_):
    try:
        return float(str_)
    except Exception as e:
        print(e)
        # 执行错误 也应该返回信息
        return None

注意我们刚才做的修改,我们只是添加了一个局部变量。在有设计良好的应用程序接口(ApplicationProgrammer Interface, API)时, 返回值可以更灵活。你可以在文档中这样写,如果传递给 str_2_float() 合适的参数,它将返回一个浮点型;如果出现错误,将返回一个字符串说明输入数据有什么问题。我们按照这个方案再修改一次代码,如下所示:

python
def str_2_float(str_):
    try:
        return float(str_)
    except ValueError:
        return '不能将一个 Nan 转化为浮点数'

        return None

这里我们只是把 None 替换为一个错误字符串。下面我们试试这个函数看看它表现如何:

python
>>> def str_2_float(str_):
...     try:
...         return float(str_)
...     except ValueError:
...         return '不能将一个 Nan 转化为浮点数'
...
>>> str_2_float('s')
'不能将一个 Nan 转化为浮点数'

我们有了一个好的开始——现在我们已经可以探测到非法的字符串输入了,可如果传递的是一个非法的对象,还是会“受伤”:

python
>>> def str_2_float(str_):
...     try:
...         return float(str_)
...     except ValueError:
...         return '不能将一个 Nan 转化为浮点数'
... 
>>> str_2_float({'a': 'Dict'})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in str_2_float
TypeError: float() argument must be a string or a real number, not 'dict'

总结

  1. 只处理你知道的异常,避免捕获所有异常然后吞掉它们。
  2. 抛出的异常应该说明原因,有时候你知道异常类型也猜不出所以然。
  3. 避免在 except 语句块中干一些没意义的事情,捕获异常也是需要成本的。
  4. 不要使用异常来控制流程,那样你的程序会无比难懂和难维护。
  5. 如果有需要,切记使用 finally 来释放资源。
  6. 如果有需要,请不要忘记在处理异常后做清理工作或者回滚操作。