错误处理与异常

Beginner

This tutorial is from open-source community. Access the source code

简介

虽然异常在前面已经介绍过,但本节将补充一些关于错误检查和异常处理的更多细节。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 初级 级别的实验,完成率为 85%。获得了学习者 100% 的好评率。

程序如何出错

Python 不会对函数参数的类型或值进行任何检查或验证。函数可以处理任何与函数中的语句兼容的数据。

def add(x, y):
    return x + y

add(3, 4)               ## 7
add('Hello', 'World')   ## 'HelloWorld'
add('3', '4')           ## '34'

如果函数中存在错误,它们会在运行时出现(作为异常)。

def add(x, y):
    return x + y

>>> add(3, '4')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +:
'int' and 'str'
>>>

为了验证代码,非常强调测试(稍后介绍)。

异常

异常用于表示错误。要自行引发异常,请使用 raise 语句。

if name not in authorized:
    raise RuntimeError(f'{name} not authorized')

要捕获异常,请使用 try-except

try:
    authenticate(username)
except RuntimeError as e:
    print(e)

异常处理

异常会传播到第一个匹配的 except 块。

def grok():
  ...
    raise RuntimeError('Whoa!')   ## 在此处引发异常

def spam():
    grok()                        ## 此调用将引发异常

def bar():
    try:
       spam()
    except RuntimeError as e:     ## 在此处捕获异常
      ...

def foo():
    try:
         bar()
    except RuntimeError as e:     ## 异常不会到达此处
      ...

foo()

要处理异常,可在 except 块中放置语句。你可以添加任何想要用来处理错误的语句。

def grok():...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   ## 在此处捕获异常
        statements              ## 使用这些语句
        statements
      ...

bar()

处理完异常后,执行会从 try-except 之后的第一条语句继续。

def grok():...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   ## 在此处捕获异常
        statements
        statements
      ...
    statements                  ## 在此处恢复执行
    statements                  ## 并在此处继续
  ...

bar()

内置异常

大约有二十几种内置异常。通常,异常的名称能表明出了什么问题(例如,引发 ValueError 是因为你提供了一个错误的值)。这并非完整列表。如需更多信息,请查看文档

ArithmeticError
AssertionError
EnvironmentError
EOFError
ImportError
IndexError
KeyboardInterrupt
KeyError
MemoryError
NameError
ReferenceError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError

异常值

异常有一个关联的值。它包含有关错误情况的更具体信息。

raise RuntimeError('Invalid user name')

此值是异常实例的一部分,该异常实例会被放入提供给 except 的变量中。

try:
 ...
except RuntimeError as e:   ## `e` 保存引发的异常
 ...

e 是异常类型的一个实例。不过,打印时它通常看起来像一个字符串。

except RuntimeError as e:
    print('Failed : Reason', e)

捕获多种错误

你可以使用多个 except 块来捕获不同类型的异常。

try:
...
except LookupError as e:
...
except RuntimeError as e:
...
except IOError as e:
...
except KeyboardInterrupt as e:
...

或者,如果处理这些异常的语句相同,你可以将它们分组:

try:
...
except (IOError,LookupError,RuntimeError) as e:
...

捕获所有错误

要捕获任何异常,可以像这样使用 Exception

try:
  ...
except Exception:       ## 危险。请见下文
    print('An error occurred')

一般来说,编写这样的代码不是个好主意,因为你根本不知道它为什么会失败。

捕获错误的错误方式

以下是使用异常的错误方式。

try:
    go_do_something()
except Exception:
    print('Computer says no')

这会捕获所有可能的错误,并且当代码由于你完全没有预料到的原因(例如,未安装的 Python 模块等)而失败时,可能会导致无法调试。

相对更好的方法

如果你打算捕获所有错误,这是一种更明智的方法。

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)

它会报告失败的具体原因。当你编写捕获所有可能异常的代码时,拥有某种查看/报告错误的机制几乎总是个好主意。

不过一般来说,尽可能精确地捕获错误会更好。只捕获你实际能够处理的错误。让其他错误通过——也许其他代码可以处理它们。

重新引发异常

使用 raise 来传播捕获到的错误。

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)
    raise

这使你能够采取行动(例如记录日志)并将错误传递给调用者。

异常处理最佳实践

不要捕获异常。要快速且明显地失败。如果问题很重要,会有其他人来处理。只有当你就是那个能处理问题的人时,才捕获异常。也就是说,只捕获那些你能够恢复并合理继续运行的错误。

finally 语句

它指定了无论是否发生异常都必须运行的代码。

lock = Lock()
...
lock.acquire()
try:
  ...
finally:
    lock.release()  ## 这将始终被执行。无论是否有异常。

通常用于安全地管理资源(特别是锁、文件等)。

with 语句

在现代代码中,try - finally 通常被 with 语句所取代。

lock = Lock()
with lock:
    ## 已获取锁
  ...
## 锁已释放

一个更常见的例子:

with open(filename) as f:
    ## 使用文件
  ...
## 文件已关闭

with 为资源定义了一个使用 _上下文_。当执行离开该上下文时,资源会被释放。with 仅适用于经过专门编程以支持它的某些对象。

练习 3.8:引发异常

你在上一节中编写的 parse_csv() 函数允许用户选择指定的列,但这仅在输入数据文件有列标题时才有效。

修改代码,以便在同时传入 selecthas_headers=False 参数时引发异常。例如:

>>> parse_csv('/home/labex/project/prices.csv', select=['name','price'], has_headers=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 9, in parse_csv
    raise RuntimeError("select argument requires column headers")
RuntimeError: select argument requires column headers
>>>

添加了这个检查之后,你可能会问是否应该在函数中执行其他类型的合理性检查。例如,是否应该检查文件名是否为字符串,types 是否为列表,或者诸如此类的事情?

一般来说,通常最好跳过此类测试,让程序在输入错误时失败。回溯消息会指出问题的根源,有助于调试。

添加上述检查的主要原因是避免代码在无意义的模式下运行(例如,使用需要列标题的功能,但同时指定没有标题)。

这表明调用代码存在编程错误。检查“不应该发生”的情况通常是个好主意。

练习 3.9:捕获异常

你编写的 parse_csv() 函数用于处理文件的全部内容。然而,在现实世界中,输入文件可能存在损坏、缺失或脏数据的情况。尝试这个实验:

>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 36, in parse_csv
    row = [func(val) for func, val in zip(types, row)]
ValueError: invalid literal for int() with base 10: ''
>>>

修改 parse_csv() 函数,捕获在创建记录期间生成的所有 ValueError 异常,并为无法转换的行打印一条警告消息。

该消息应包括行号以及失败原因的相关信息。为了测试你的函数,尝试读取上面的 missing.csv 文件。例如:

>>> portfolio = parse_csv('missing.csv', types=[str, int, float])
第4行:无法转换 ['MSFT', '', '51.23']
第4行:原因是int() 无法将空字符串转换为十进制整数
第7行:无法转换 ['IBM', '', '70.44']
第7行:原因是int() 无法将空字符串转换为十进制整数
>>>
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}]
>>>

练习 3.10:抑制错误

修改 parse_csv() 函数,以便在用户明确希望时可以抑制解析错误消息。例如:

>>> portfolio = parse_csv('missing.csv', types=[str,int,float], silence_errors=True)
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}]
>>>

在大多数程序中,错误处理是最难做好的事情之一。一般来说,你不应该默默地忽略错误。相反,最好报告问题,并给用户一个选择,如果他们愿意,可以抑制错误消息。

总结

恭喜你!你已经完成了错误检查实验。你可以在 LabEx 中练习更多实验来提升你的技能。