Python使用名为”异常”的特殊对象来管理程序执行期间发生的错误。

语法错误

语法错误,也叫作解析错误,这可能是你在学习Python的过程中,最容易碰到的错误:

1
2
3
4
5
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^
SyntaxError: invalid syntax

解析结果显示出错的行代码,并用小箭头指明解析到错误的具体位置。
这个错误是在箭头所指向的位置:在本例中,错误是在这个函数中检测到的 print(),因为print函数之前应该存在的冒号缺失。文件名称和行号都被打印出来,这样你就可以知道这行有错误的代码是在哪个位置了。

异常

一条语句或表达式即便是语法正确也有可能在运行的时候报错。程序执行过程中遇到的错误被称为异常。这种错误并不一定是致命错误,我们一会儿来看如何在程序中处理异常。大多数的异常并非由程序自身处理,而是会显示类似下面列举的错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly

错误信息的最后一行告诉我们程序遇到了什么类型的错误。异常有不同的类型,而其类型名称将会在错误信息中输出出来。上述样例中的异常类型依次是: ZeroDivisionErrorNameError 以及 TypeError 。错误信息中的异常类型是执行时抛出的内建异常类型。所有的内建类型都会如此,而虽然这是一个有用的惯例,但是用户自定义的异常并非一定如此。标准的异常类型是内建的标识符而非预留关键词。

这一行剩下的部分能告诉我们此处抛出异常的具体信息及触发的原因。

错误信息的前面部分能够通过栈调用信息告诉我们抛出异常时的上下文。一般情况下栈调用信息会包含函数调用所在的源代码行,但是不会包含从标准输入读入的信息。

Built-in Exceptions 列举了内建异常类型以及各自的含义。

捕捉异常

Python 允许编程处理特定的异常。在下面这个例子中,程序要求用户进行输入,直到接收到一个合法的整数,同时也允许用户中断程序 (使用 Control-C 或操作系统支持的其他方式);注意,由用户引起的中断通过抛出 KeyboardInterrupt 异常来实现。

1
2
3
4
5
6
7
>>> while True:
... try:
... x = int(input("Please enter a number: "))
... break
... except ValueError:
... print("Oops! That was no valid number. Try again...")
...

try声明的工作原理如下:

  • 程序首先执行 try 子句 (位于 tryexcept 关键字之间的内容)。
  • 如果没有异常产生,则 except 子句被跳过,并且 try 声明的部分运行结束
  • 如果在执行 try 子句的过程中产生了一个异常,那么这个子句范围内产生异常位置之后的代码不会被执行,如果产生的异常位于 except 关键字后提到的若干异常之中,那么except 子句的内容将会接着 try 中刚被中止的位置继续执行。
  • 如果产生的异常并不在 exception 后面包含的若干异常中,该异常将会被抛给上一层的 try 语句;如果一个异常没有被任何一层 try-exception 语句捕捉,它就成为一个 未处理异常 ,程序执行将因此停止并显示如上所示的消息。
    一个 try 声明可以包含多个 except 子句来针对不同的异常做出不同的处理,但最多只有一个 except 子句(即错误处理器)会被执行。错误处理器只处理出现在对应 try 子句中产生的异常,而不会处理同一 try 语句中其他错误处理器中的异常。一个 except 子句可以指定多个异常,这些异常用一个元组包含起来,例如:
1
2
... except (RuntimeError, TypeError, NameError):
... pass

捕获所有异常

想要捕获所有的异常,可以直接捕获 Exception 即可:

1
2
3
4
5
try:
...
except Exception as e:
...
log('Reason:', e) # Important!

这个将会捕获除了 SystemExitKeyboardInterruptGeneratorExit 之外的所有异常。 如果你还想捕获这三个异常,将 Exception 改成 BaseException 即可。

讨论

捕获所有异常通常是由于程序员在某些复杂操作中并不能记住所有可能的异常。 如果你不是很细心的人,这也是编写不易调试代码的一个简单方法。

正因如此,如果你选择捕获所有异常,那么在某个地方(比如日志文件、打印异常到屏幕)打印确切原因就比较重要了。 如果你没有这样做,有时候你看到异常打印时可能摸不着头脑,就像下面这样:

1
2
3
4
5
def parse_int(s):
try:
n = int(v)
except Exception:
print("Couldn't parse")

试着运行这个函数,结果如下:

1
2
3
4
5
>>> parse_int('n/a')
Couldn't parse
>>> parse_int('42')
Couldn't parse
>>>

这时候你就会挠头想:“这咋回事啊?” 假如你像下面这样重写这个函数:

1
2
3
4
5
6
def parse_int(s):
try:
n = int(v)
except Exception as e:
print("Couldn't parse")
print('Reason:', e)

这时候你能获取如下输出,指明了有个编程错误:

1
2
3
4
>>> parse_int('42')
Couldn't parse
Reason: global name 'v' is not defined
>>>

很明显,你应该尽可能将异常处理器定义的精准一些。 不过,要是你必须捕获所有异常,确保打印正确的诊断信息或将异常传播出去,这样不会丢失掉异常。

练习题:python实现tmp目录下匹配文件,并打包压缩至系统另一个目录,关键模块:tarfile、fnmatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python
# vim:set et ts=4 sw=4 fileencoding=utf-8:
# -*- coding: utf-8 -*-
# @Author: yo

import os
import tarfile
import fnmatch

src_dir = '/tmp'


def tar_file():
try:
if os.getcwd() not in src_dir:
os.chdir(src_dir)
print (os.getcwd())
with tarfile.open("/tmp/tmp-bak/bak.tar.gz", "w:gz") as tar_package:
for files in os.listdir(os.getcwd()):
if fnmatch.fnmatch(files, '*_*.jpg'):
tar_package.add(files)
tar_package.close()
except:
print ("Tar Archive Error..")
raise


if __name__ == '__main__':
tar_file()

直接使用raise也能正常抛出异常。

如果两个异常是同一类或者具有同一个父类,那它们可以在一个 except 子句中同时存在(但是另一种情况并不成立——如果一个类是另一个类的派生类,那么这两个类不能在一个 except 子句中同时存在)。例如,下面的这段代码中,将按照顺序输出 B, C, D

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class B(Exception):
pass

class C(B):
pass

class D(C):
pass

for cls in [B, C, D]:
try:
raise cls()
except D:
print("D")
except C:
print("C")
except B:
print("B")

注意:如果这些 except 子句按照相反的顺序排列(except B在最前面),则输出会变成 B,B,B。——和异常匹配的第一个 except 子句会被触发。

最后一个 except 子句可以省略异常名用作通配符。使用这个方法要特别谨慎,因为这个方法可能掩盖一个真正的编程错误!它还可以用于打印错误后,重新引发异常(也允许调用者处理异常):

1
2
3
4
5
6
7
8
9
10
11
12
13
import sys

try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("OS error: {0}".format(err))
except ValueError:
print("Could not convert data to an integer.")
except:
print("Unexpected error:", sys.exc_info()[0])
raise

tryexcept 语句有一个可选的 else 子句, 当它出现时,必须在所有l except 子句之后。在执行 try 子句之后没有引发异常因此时,它可以用于必须执行的代码。例如:

1
2
3
4
5
6
7
8
for arg in sys.argv[1:]:
try:
f = open(arg, 'r')
except OSError:
print('cannot open', arg)
else:
print(arg, 'has', len(f.readlines()), 'lines')
f.close()

else 子句的使用比在 try 子句中增加而外的代码更好,因为他可以避免意外地捕获不被 tryexcept 语句包含的异常。

当一个异常发生时,它可能有相关的值,或者是异常的 参数。尝试的有无和参数的类型取决于异常的类型。

except 子句可以在异常名之后指定变量。这些变量被绑定到一个异常实例中,实例中的参数储存在 instance.args 中。为了方便,异常实例定义了 __str__() ,所以参数可以被直接打印出来,而不需要引用 .args 。还可以在引发异常之前先实例化异常并根据需要添加任何属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> try:
... raise Exception('spam', 'eggs')
... except Exception as inst:
... print(type(inst)) # 异常实例
... print(inst.args) # 储存在 .args 中的参数
... print(inst) # __str__ 允许参数被直接打印,
... # 但是方法可能会被异常子类复写
... x, y = inst.args # 提取参数
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

如果异常带有参数,他们会作为未处理异常的信息中的最后一部分(’detail’)被打印出来。

异常处理程序不仅仅立刻处理 try 子句中的产生的异常,同时也包括 try 子句中调用函数(即使是间接地)中产生的异常。例如:

1
2
3
4
5
6
7
8
9
>>> def this_fails():
... x = 1/0
...
>>> try:
... this_fails()
... except ZeroDivisionError as err:
... print('Handling run-time error:', err)
...
Handling run-time error: division by zero

抛出异常

raise 语句允许开发者显式地引发异常。例如:

1
2
3
4
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: HiThere

raise 关键字后跟随表示被抛出异常的单一变量,该变量可以是一个异常实例,或者一个异常类(继承自 Exception)。如果被传递的变量是一个异常类,它会隐式地无参数调用其构造方法进行实例化:

1
raise ValueError  # 'raise ValueError()' 的简写

如果你只需要知道一个异常被抛出了,但并需要处理它,可以通过一个简单的 raise 语句重新抛出这个异常:

1
2
3
4
5
6
7
8
9
10
>>> try:
... raise NameError('HiThere')
... except NameError:
... print('An exception flew by!')
... raise
...
An exception flew by!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
NameError: HiThere

用户定义的异常

程序可以通过创建一个新的异常类来命名其专属的异常 (参考 Classes 以获得更多有关Python类的信息)。 异常通常应该派生自 Exception 类,无论是直接地还是间接地。

异常类可以以如通常类一样进行定义,但是通常会保持简洁,一般仅仅提供数个用于异常处理时可以解析异常信息的属性。在创建一个可以能引起多个不同异常的模块时,通常的做法是创建一个由模块定义的异常基类,并为不同的异常条件创建特定的异常子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Error(Exception):
"""本模块的异常基类"""
pass

class InputError(Error):
"""输入中错误引发的异常

属性:
expression -- 产生错误输入表达
message -- 错误解释
"""

def __init__(self, expression, message):
self.expression = expression
self.message = message

class TransitionError(Error):
"""尝试不允许状态转换的操作时引发的异常

属性:
previous -- 转换前的状态
next -- 尝试转换的状态
message -- 解释为什么特定的转换操作不允许
"""

def __init__(self, previous, next, message):
self.previous = previous
self.next = next
self.message = message

大多数的异常定义都以「错误(Error)」结尾,和标准异常的命名类似。

很多标准模块定义了它们自己的异常,用于报告它们定义的函数中可能产生的错误。有关类的更多信息在章节 Classes 中有介绍.

定义清理操作

try 语句有另一个可选的子语句,用于定义必须在所有情况下都执行的清理操作。例如:

1
2
3
4
5
6
7
8
9
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Goodbye, world!')
...
Goodbye, world!
KeyboardInterrupt
Traceback (most recent call last):
File "<stdin>", line 2, in <module>

finally 子句 总是在结束 try 语句之前执行,无论是否有异常产生。当异常产生在When an exception try 子句中产生并未被 except 子句捕获(或异常在 exceptelse 子句中产生)时,异常将在 finally子句被执行后再引发。 finally 子句也在「离开的路上」被执行,即当其他子句通过 break, continue or return 等语句离开 try 语句时。以下为一个更加复杂的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> def divide(x, y):
... try:
... result = x / y
... except ZeroDivisionError:
... print("division by zero!")
... else:
... print("result is", result)
... finally:
... print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

如你所见, finally 子句在任一事件中都被执行。 TypeError 异常因进行字符串除法引发,而且未被 except 子句捕获,并因此在 finally 子句被执行后之后才产生。

在实际的应用中, finally 子句在释放外部资源(如文件或者网络连接)时非常非常有用,无论资源是否被成功使用。

预定义的清理操作

某些对象定义了在不再需要该对象时需要执行的标准清理操作,无论在该对象上进行的操作是成功还是失败。 查看以下示例,该示例尝试打开文件并将其内容打印到屏幕上。

1
2
for line in open("myfile.txt"):
print(line, end="")

此代码的问题在于,在部分代码执行完毕后,它会使文件保持打开一段不确定的时间。 这在简单脚本中不是问题,但对于较大的应用可能是一个问题。 with 语句使用允许文件之类的对象是,以保证始终及时正确的清理的方式进行。

1
2
3
with open("myfile.txt") as f:
for line in f:
print(line, end="")

在语句执行后,文件 f 总是被关闭,即使在处理每行时遇到错误。 与文件相同,提供预定义清理操作的对象将在其文档中指出这一点。

参考资料