10 避免except:子句带来的潜在问题
在异常处理中有一些常见的错误,这些错误可能导致程序无响应。
最常见的一种错误就是except:子句使用不当。如果不谨慎处理现有异常,那么可能还会出现其他错误。
本实例将介绍一些可以避免的常见异常处理错误。
10.1 准备工作
前面介绍了设计异常处理时的一些注意事项。该实例不鼓励使用BaseException,因为可能会干扰终止行为异常的Python程序。
本实例将扩展不该做什么(what not to do)思想。
10.2 实战演练
使用except Exception:语句作为最通用的异常管理。
处理过多的异常可能会干扰终止行为异常的Python程序的能力。当按下Ctrl + C,或通过kill -2发送SIGINT信号时,我们通常希望终止程序,而不是让程序写入消息并继续运行,或者完全停止响应。
其他应当谨慎处理的异常类包括:
SystemError
RuntimeError
MemoryError
这些异常通常意味着Python内核的某个地方出现了问题。与其静默这些异常或尝试恢复,不如终止程序,找到根本原因,并解决问题。
10.3 工作原理
在处理异常时应当注意以下两个问题。
不要捕获BaseException类。
不要使用不指明异常类的except: 语句。except: 语句将匹配所有异常,包括应当避免处理的异常。
使用except BaseException或不指明特定类的except语句可能导致程序在需要终止时变得无响应。
退一步讲,如果捕获了所有异常,那么还可以干扰这些内部异常的处理方式:
SystemExit
KeyboardInterrupt
GeneratorExit
如果静默、包装或重写这些异常,那么就可能引入新问题,甚至将一个简单的问题复杂化。
编写从不崩溃的程序是崇高的理想。干扰Python的某些内部异常并不会创建更可靠的程序。相反,明显的错误被掩盖了,这些错误变成了难解之谜。
11 使用raise from语句链接异常
在某些情况下,我们需要将一些看似不相关的异常合并为一个通用异常。对于复杂的模块来说,定义一个通用的Error异常是很常见的,该异常适用于模块中可能出现的多种情况。
在大多数情况下,通用异常都是必须的。如果抛出模块的Error异常,那么程序的某些功能将不能正常运行。
在极少数情况下,出于调试或监控的目的,我们希望得到异常的详细信息。我们也许想将这些详细信息写入日志,或者包含在电子邮件中。在这种情况下,我们需要提供放大或扩展通用异常的详细信息。可以通过将通用异常链接到根源异常来实现这种功能。
11.1 准备工作
假设我们正在编写一些复杂的字符串处理,希望将多种不同类型的详细异常视为一个通用异常,以便隔离软件用户与实施细节。我们可以将详细异常附加到通用异常。
11.2 实战演练
(1) 创建一个新的异常,如下所示:
class Error(Exception):
pass
新的异常类就定义好了。
(2) 在处理异常时,可以使用raise from语句链接异常类,如下所示:
try:
something
except (IndexError, NameError) as exception:
print("Expected", exception)
raise Error("something went wrong") from exception
except Exception as exception:
print("Unexpected", exception)
raise
第一个except子句匹配了两种类型的异常类。无论捕获到哪种类型的异常,都会从模块的通用Error异常类中抛出一个新的异常。新的异常将链接到根源异常。第二个except子句匹配了通用的Exception类。我们首先写入了一个日志消息,然后重新抛出了异常。第二个except子句没有链接异常,而是在另一个上下文中继续异常处理。
11.3 工作原理
Python异常类的__cause__属性可以记录异常的原因。可以使用raise Exception from Exception语句来设置__cause__属性。
抛出这种异常时的详细信息如下所示:
>>> class Error(Exception):
... pass
>>> try:
... 'hello world'[99]
... except (IndexError, NameError) as exception:
... raise Error("index problem") from exception
...
Traceback (most recent call last):
File "<doctest default[0]>", line 2, in <module>
'hello world'[99]
IndexError: string index out of range
上述异常是以下异常的直接原因:
Traceback (most recent call last):
File "/Library/Frameworks/Python.framework/Versions/3.4/lib/python3.4/ doctest.py", line 1318, in __run
compileflags, 1), test.globs)
File "<doctest default[0]>", line 4, in <module>
raise Error("index problem") from exception
Error: index problem
上述示例展示了一个相互链接的异常。Traceback消息中的第一个异常是IndexError异常,这是直接原因。Traceback中的第二个异常是通用Error异常,这是一个链接到原始原因的通用汇总异常。
应用程序将在try: 语句中抛出Error异常。代码如下所示:
try:
some_function()
except Error as exception:
print(exception)
print(exception .__cause__)
这个示例包含了一个名为some_function()的函数,它可以抛出通用的Error异常。如果该函数确实抛出异常,则except子句将匹配通用的Error异常。我们可以打印异常的消息exception,以及异常的根本原因exception.cause。在许多应用程序中,exception.__cause__值可能会被写入调试日志,而不是显示给用户。
11.4 补充知识
如果在异常处理代码中抛出异常,那么也会创建一种异常链接关系。这是一种上下文(context)关系,而不是原因(cause)关系。
上下文消息看起来都很相似,只是消息略有不同。这说明在处理上一个异常时,发生了另一个异常。第一个Traceback显示了原始异常。第二个消息是未使用显式连接抛出的异常。
通常,上下文是无计划的,只是表示except异常处理块中出现错误。例如:
try:
something
except ValueError as exception:
print("Some message", exceotuib)
上述示例通过ValueError异常的上下文抛出一个NameError异常。NameError异常源于异常变量被错误地拼写为exceotuib。
12 使用with语句管理上下文
在许多情况下,脚本需要使用外部资源,最常见的例子就是磁盘文件和与外部主机的网络连接。没有及时断开外部资源的绑定,导致资源无法释放是一种常见的bug。这种bug有时被称为内存泄漏,因为每次打开一个新文件时都不关闭以前使用的文件,所以导致可用内存减少。
我们想隔离资源的绑定,确保正确获取和释放资源,其中一种解决方法就是创建脚本使用外部资源的上下文。在上下文结束时,程序不再与资源绑定,我们希望确保资源被释放。
12.1 准备工作
假设我们想向一个CSV格式的文件写入几行数据。完成写入后,想要确保文件关闭,并释放各种操作系统资源(包括缓冲区和文件句柄)。上下文管理器可以实现这个设想,它可以确保文件正确关闭。
因为需要处理CSV文件,所以可以使用CSV模块处理格式化的细节:
>>> import csv
另外还需要使用pathlib模块查找待处理的文件:
>>> import pathlib
为了模拟写入,我们还准备了虚拟的数据源:
>>> some_source = [[2,3,5], [7,11,13], [17,19,23]]
上述代码提供了一个了解with语句的情景。
12.2 实战演练
(1) 通过打开文件或使用urllib.request.urlopen()创建网络连接创建上下文。其他常见的上下文包括zip文件和tar文件。
target_path = pathlib.Path('code/test.csv')
with target_path.open('w', newline='') as target_file:
(2) 添加所有处理过程,确保在with语句内缩进。
target_path = pathlib.Path('code/test.csv')
with target_path.open('w', newline='') as target_file:
writer = csv.writer(target_file)
writer.writerow(['column', 'data', 'headings'])
for data in some_source:
writer.writerow(data)
(3) 在使用文件作为上下文管理器时,文件将在缩进的上下文代码块执行结束后自动关闭。即使出现异常,文件仍然正常关闭。在关闭上下文并释放资源后,继续执行上下文代码块下面的处理。
target_path = pathlib.Path('code/test.csv')
with target_path.open('w', newline='') as target_file:
writer = csv.writer(target_file)
writer.writerow(['column', 'headings'])
for data in some_source:
writer.writerow(data)
print('finished writing', target_path)
with上下文之外的语句将在上下文关闭后执行,由target_path.open()打开的文件将正确关闭。
即使在with语句内部抛出异常,文件仍然正确关闭。虽然上下文管理器已经得到了异常信息,但是它仍然可以关闭文件并允许传播异常。
12.3 工作原理
代码通知上下文管理器退出的方式有两种:
没有异常,正常退出;
抛出异常。
在任何情况下,上下文管理器都将程序与外部资源分开,可以关闭文件、删除网络连接、提交或回滚数据库事务,以及释放锁。
可以通过在with语句内添加手动异常来进行验证。下面的代码可以证明文件正确关闭:
try:
target_path = pathlib.Path('code/test.csv')
with target_path.open('w', newline='') as target_file:
writer = csv.writer(target_file)
writer.writerow(['column', 'headings'])
for data in some_source:
writer.writerow(data)
raise Exception("Just Testing")
except Exception as exc:
print(target_file.closed)
print(exc)
print('finished writing', target_path)
在本例中,核心处理包装在try语句中。因此,在第一次写入CSV文件之后就可以抛出异常。抛出异常时,可以打印输出异常。此时,文件也将被关闭。输出如下所示:
True
Just Testing
finished writing code/test.csv
这个例子说明了文件被正确关闭,另外还展示了异常相关的消息,以确认该异常是我们手动抛出的异常。输出文件test.csv只有从变量some_source中获取的第一行数据。
12.4 补充知识
Python提供了多种上下文管理器。打开的文件是一种上下文管理器,由urllib.request.urlopen()
创建的网络连接同样也是一种上下文管理器。
对于所有文件操作和网络连接,都应该通过with语句用作上下文管理器。我们很难找到关于这一规则的一种例外情况。
实际上,因为decimal模块使用了上下文管理器,所以允许对小数运算的方法进行局部修改。可以使用decimal.localcontext()函数作为上下文管理器,改变由with语句隔离的舍入规则或精度。
还可以自定义上下文管理器。contextlib模块包含很多函数和装饰器,可以帮助我们根据资源创建上下文管理器,而不是显式地提供资源。
当使用锁时,with上下文管理器是获取和释放锁的理想方式。关于threading模块创建的锁对象与上下文管理器之间的关系,请参阅https://docs.python.org/3/library/threading.html#with-locks。
12.5 延伸阅读
关于with语句的起源,请参阅https://www.python.org/dev/peps/pep-0343/。