时长08:47大小8.06M
你好,我是景霄。
今天这节课,我想和你聊聊 Python 的异常处理。和其他语言一样,异常处理是 Python 中一种很常见,并且很重要的机制与代码规范。
我在实际工作中,见过很多次这样的情况:一位工程师提交了代码,不过代码某处忘记了异常处理。碰巧这种异常发生的频率不低,所以在代码 push 到线上后没多久,就会收到紧急通知——服务器崩溃了。
如果事情严重,对用户的影响也很大,这位工程师还得去专门的会议上做自我检讨,可以说是很惨了。这类事件层出不穷,也告诉我们,正确理解和处理程序中的异常尤为关键。
首先要了解,Python 中的错误和异常是什么?两者之间又有什么联系和区别呢?
通常来说,程序中的错误至少包括两种,一种是语法错误,另一种则是异常。
所谓语法错误,你应该很清楚,也就是你写的代码不符合编程规范,无法被识别与执行,比如下面这个例子:
if name is not None print(name)复制代码
If 语句漏掉了冒号,不符合 Python 的语法规范,所以程序就会报错invalid syntax
。
而异常则是指程序的语法正确,也可以被执行,但在执行过程中遇到了错误,抛出了异常,比如下面的 3 个例子:
10 / 0 Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: integer division or modulo by zero order * 2 Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'order' is not defined 1 + [1, 2] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'int' and 'list'复制代码
它们语法完全正确,但显然,我们不能做除法时让分母为 0;也不能使用未定义的变量做运算;而让一个整型和一个列表相加也是不可取的。
于是,当程序运行到这些地方时,就抛出了异常,并且终止运行。例子中的ZeroDivisionError
NameError
和TypeError
,就是三种常见的异常类型。
当然,Python 中还有很多其他异常类型,比如KeyError
是指字典中的键找不到;FileNotFoundError
是指发送了读取文件的请求,但相应的文件不存在等等,我在此不一一赘述,你可以自行参考相应文档。
刚刚讲到,如果执行到程序中某处抛出了异常,程序就会被终止并退出。你可能会问,那有没有什么办法可以不终止程序,让其照样运行下去呢?答案当然是肯定的,这也就是我们所说的异常处理,通常使用 try 和 except 来解决,比如:
try: s = input('please enter two numbers separated by comma: ') num1 = int(s.split(',')[0].strip()) num2 = int(s.split(',')[1].strip()) ... except ValueError as err: print('Value Error: {}'.format(err)) print('continue') ...复制代码
这里默认用户输入以逗号相隔的两个整形数字,将其提取后,做后续的操作(注意 input 函数会将输入转换为字符串类型)。如果我们输入a,b
,程序便会抛出异常invalid literal for int() with base 10: 'a'
,然后跳出 try 这个 block。
由于程序抛出的异常类型是 ValueError,和 except block 所 catch 的异常类型相匹配,所以 except block 便会被执行,最终输出Value Error: invalid literal for int() with base 10: 'a'
,并打印出continue
。
please enter two numbers separated by comma: a,b Value Error: invalid literal for int() with base 10: 'a' continue复制代码
我们知道,except block 只接受与它相匹配的异常类型并执行,如果程序抛出的异常并不匹配,那么程序照样会终止并退出。
所以,还是刚刚这个例子,如果我们只输入1
,程序抛出的异常就是IndexError: list index out of range
,与 ValueError 不匹配,那么 except block 就不会被执行,程序便会终止并退出(continue 不会被打印)。
please enter two numbers separated by comma: 1 IndexError Traceback (most recent call last) IndexError: list index out of range复制代码
不过,很显然,这样强调一种类型的写法有很大的局限性。那么,该怎么解决这个问题呢?
其中一种解决方案,是在 except block 中加入多种异常的类型,比如下面这样的写法:
try: s = input('please enter two numbers separated by comma: ') num1 = int(s.split(',')[0].strip()) num2 = int(s.split(',')[1].strip()) ... except (ValueError, IndexError) as err: print('Error: {}'.format(err)) print('continue') ...复制代码
或者第二种写法:
try: s = input('please enter two numbers separated by comma: ') num1 = int(s.split(',')[0].strip()) num2 = int(s.split(',')[1].strip()) ... except ValueError as err: print('Value Error: {}'.format(err)) except IndexError as err: print('Index Error: {}'.format(err)) print('continue') ...复制代码
这样,每次程序执行时,except block 中只要有一个 exception 类型与实际匹配即可。
不过,很多时候,我们很难保证程序覆盖所有的异常类型,所以,更通常的做法,是在最后一个 except block,声明其处理的异常类型是 Exception。Exception 是其他所有非系统异常的基类,能够匹配任意非系统异常。那么这段代码就可以写成下面这样:
try: s = input('please enter two numbers separated by comma: ') num1 = int(s.split(',')[0].strip()) num2 = int(s.split(',')[1].strip()) ... except ValueError as err: print('Value Error: {}'.format(err)) except IndexError as err: print('Index Error: {}'.format(err)) except Exception as err: print('Other error: {}'.format(err)) print('continue') ...复制代码
或者,你也可以在 except 后面省略异常类型,这表示与任意异常相匹配(包括系统异常等):
try: s = input('please enter two numbers separated by comma: ') num1 = int(s.split(',')[0].strip()) num2 = int(s.split(',')[1].strip()) ... except ValueError as err: print('Value Error: {}'.format(err)) except IndexError as err: print('Index Error: {}'.format(err)) except: print('Other error') print('continue') ...复制代码
需要注意,当程序中存在多个 except block 时,最多只有一个 except block 会被执行。换句话说,如果多个 except 声明的异常类型都与实际相匹配,那么只有最前面的 except block 会被执行,其他则被忽略。
异常处理中,还有一个很常见的用法是 finally,经常和 try、except 放在一起来用。无论发生什么情况,finally block 中的语句都会被执行,哪怕前面的 try 和 excep block 中使用了 return 语句。
一个常见的应用场景,便是文件的读取:
import sys try: f = open('file.txt', 'r') .... # some data processing except OSError as err: print('OS error: {}'.format(err)) except: print('Unexpected error:', sys.exc_info()[0]) finally: f.close()复制代码
这段代码中,try block 尝试读取 file.txt 这个文件,并对其中的数据进行一系列的处理,到最后,无论是读取成功还是读取失败,程序都会执行 finally 中的语句——关闭这个文件流,确保文件的完整性。因此,在 finally 中,我们通常会放一些无论如何都要执行的语句。
值得一提的是,对于文件的读取,我们也常常使用 with open,你也许在前面的例子中已经看到过,with open 会在最后自动关闭文件,让语句更加简洁。
前面的例子里充斥了很多 Python 内置的异常类型,你可能会问,我可以创建自己的异常类型吗?
答案是肯定是,Python 当然允许我们这么做。下面这个例子,我们创建了自定义的异常类型 MyInputError,定义并实现了初始化函数和 str 函数(直接 print 时调用):
class MyInputError(Exception): """Exception raised when there're errors in input""" def __init__(self, value): # 自定义异常类型的初始化 self.value = value def __str__(self): # 自定义异常类型的 string 表达形式 return ("{} is invalid input".format(repr(self.value))) try: raise MyInputError(1) # 抛出 MyInputError 这个异常 except MyInputError as err: print('error: {}'.format(err))复制代码
如果你执行上述代码块并输出,便会得到下面的结果:
error: 1 is invalid input复制代码
实际工作中,如果内置的异常类型无法满足我们的需求,或者为了让异常更加详细、可读,想增加一些异常类型的其他功能,我们可以自定义所需异常类型。不过,大多数情况下,Python 内置的异常类型就足够好了。
学完了前面的基础知识,接下来我们着重谈一下,异常的使用场景与注意点。
通常来说,在程序中,如果我们不确定某段代码能否成功执行,往往这个地方就需要使用异常处理。除了上述文件读取的例子,我可以再举一个例子来说明。
大型社交网站的后台,需要针对用户发送的请求返回相应记录。用户记录往往储存在 key-value 结构的数据库中,每次有请求过来后,我们拿到用户的 ID,并用 ID 查询数据库中此人的记录,就能返回相应的结果。
而数据库返回的原始数据,往往是 json string 的形式,这就需要我们首先对 json string 进行 decode(解码),你可能很容易想到下面的方法:
import json raw_data = queryDB(uid) # 根据用户的 id,返回相应的信息 data = json.loads(raw_data)复制代码
这样的代码是不是就足够了呢?
要知道,在 json.loads() 函数中,输入的字符串如果不符合其规范,那么便无法解码,就会抛出异常,因此加上异常处理十分必要。
try: data = json.loads(raw_data) .... except JSONDecodeError as err: print('JSONDecodeError: {}'.format(err))复制代码
不过,有一点切记,我们不能走向另一个极端——滥用异常处理。
比如,当你想要查找字典中某个键对应的值时,绝不能写成下面这种形式:
d = {'name': 'jason', 'age': 20} try: value = d['dob'] ... except KeyError as err: print('KeyError: {}'.format(err))复制代码
诚然,这样的代码并没有 bug,但是让人看了摸不着头脑,也显得很冗余。如果你的代码中充斥着这种写法,无疑对阅读、协作来说都是障碍。因此,对于 flow-control(流程控制)的代码逻辑,我们一般不用异常处理。
字典这个例子,写成下面这样就很好。
if 'dob' in d: value = d['dob'] ...复制代码
这节课, 我们一起学习了 Python 的异常处理及其使用场景,你需要重点掌握下面几点。
异常,通常是指程序运行的过程中遇到了错误,终止并退出。我们通常使用 try except 语句去处理异常,这样程序就不会被终止,仍能继续执行。
处理异常时,如果有必须执行的语句,比如文件打开后必须关闭等等,则可以放在 finally block 中。
异常处理,通常用在你不确定某段代码能否成功执行,也无法轻易判断的情况下,比如数据库的连接、读取等等。正常的 flow-control 逻辑,不要使用异常处理,直接用条件语句解决就可以了。
最后,给你留一个思考题。在异常处理时,如果 try block 中有多处抛出异常,需要我们使用多个 try except block 吗?以数据库的连接、读取为例,下面两种写法,你觉得哪种更好呢?
第一种:
try: db = DB.connect('<db path>') # 可能会抛出异常 raw_data = DB.queryData('<viewer_id>') # 可能会抛出异常 except (DBConnectionError, DBQueryDataError) err: print('Error: {}'.format(err))复制代码
第二种:
try: db = DB.connect('<db path>') # 可能会抛出异常 try: raw_data = DB.queryData('<viewer_id>') except DBQueryDataError as err: print('DB query data error: {}'.format(err)) except DBConnectionError as err: print('DB connection error: {}'.format(err))复制代码
欢迎在留言区写下你的答案,还有你今天学习的心得和疑惑,也欢迎你把这篇文章分享给你的同事、朋友。
作者回复: 正解
作者回复: 你可以阅读官方文档:https://docs.python.org/3/reference/compound_stmts.html#the-try-statement
"When an exception has been assigned using as target, it is cleared at the end of the except clause."
比如下面这个code block:
except E as N:
foo
就等于
except E as N:
try:
foo
finally:
del N
因此你例子中的e最后被delete了,所以会抛出NameError
作者回复: 我会在最后一章里对大公司开发的规范,流程做一个详细的介绍。通常来说,异常能用内置的exception就用,如果需要自定义就自定义,看实际的需求。一般来说异常抛出,我们都会对其进行Log(一般每1000次log一次),输出到real time的table和dashboard里,这样有利于之后的分析和改进。
作者回复: 正解
作者回复: 系统异常比如说keyboardInterrupt