![a91cba98131d82cfe3b91eefb21c5dd2.png](https://img-blog.csdnimg.cn/img_convert/a91cba98131d82cfe3b91eefb21c5dd2.png)
文 | piglei 编辑 | EarlGrey
推荐 | 编程派(微信ID:codingpy)
前言
如果你用 Python 编程,那么你就无法避开异常,因为异常在这门语言里无处不在。打个比方,当你在脚本执行时按 ctrl+c
退出,解释器就会产生一个KeyboardInterrupt
异常。而KeyError
、ValueError
、TypeError
等更是日常编程里随处可见的老朋友。
异常处理工作由“捕获”和“抛出”两部分组成。“捕获”指的是使用 try...except
包裹特定语句,妥当的完成错误流程处理。而恰当的使用raise
主动“抛出”异常,更是优雅代码里必不可少的组成部分。
在这篇文章里,我会分享与异常处理相关的 3 个好习惯。继续阅读前,我希望你已经了解了下面这些知识点:
异常的基本语法与用法(建议阅读官方文档 “Errors and Exceptions”)
为什么要使用异常代替错误返回(建议阅读《让函数返回结果的技巧》)
为什么在写 Python 时鼓励使用异常 (建议阅读 “Write Cleaner Python: Use Exceptions”)
![897ce9d7fb796057370625330a583d60.png](https://img-blog.csdnimg.cn/img_convert/897ce9d7fb796057370625330a583d60.png)
三个好习惯
1. 只做最精确的异常捕获
假如你不够了解异常机制,就难免会对它有一种天然恐惧感。你可能会觉得:异常是一种不好的东西,好的程序就应该捕获所有的异常,让一切都平平稳稳的运行。而抱着这种想法写出的代码,里面通常会出现大段含糊的异常捕获逻辑。
让我们用一段可执行脚本作为样例:
# -*- coding: utf-8 -*-
import requests
import re
def save_website_title(url, filename):
"""获取某个地址的网页标题,然后将其写入到文件中
:returns: 如果成功保存,返回 True,否则打印错误,返回 False
"""
try:
resp = requests.get(url)
obj = re.search(r'(.*)', resp.text)
if not obj:
print('save failed: title tag not found in page content')
return False
title = obj.grop(1)
with open(filename, 'w') as fp:
fp.write(title)
return True
except Exception:
print(f'save failed: unable to save title of {url} to {filename}')
return False
def main:
save_website_title('https://www.qq.com', 'qq_title.txt')
if __name__ == '__main__':
main
脚本里的 save_website_title
函数做了好几件事情。它首先通过网络获取网页内容,然后利用正则匹配出标题,最后将标题写在本地文件里。而这里有两个步骤很容易出错:网络请求与本地文件操作。所以在代码里,我们用一个大大的try...except
语句块,将这几个步骤都包裹了起来。安全第一⛑。
那么,这段看上去简洁易懂的代码,里面藏着什么问题呢?
如果你旁边刚好有一台安装了 Python 的电脑,那么你可以试着跑一遍上面的脚本。你会发现,上面的代码是不能成功执行的。而且你还会发现,无论你如何修改网址和目标文件的值,程序仍然会报错 “save failed: unable to...”。为什么呢?
问题就藏在这个硕大无比的 try...except
语句块里。假如你把眼睛贴近屏幕,非常仔细的检查这段代码。你会发现在编写函数时,我犯了一个小错误,我把获取正则匹配串的方法错打成了obj.grop(1)
,少了一个 'u'(obj.group(1)
)。
但正是因为那个过于庞大、含糊的异常捕获,这个由打错方法名导致的原本该被抛出的 AttibuteError
却被吞噬了。从而给我们的 debug 过程增加了不必要的麻烦。
异常捕获的目的,不是去捕获尽可能多的异常。假如我们从一开始就坚持:只做最精准的异常捕获。那么这样的问题就根本不会发生,精准捕获包括:
永远只捕获那些可能会抛出异常的语句块
尽量只捕获精确的异常类型,而不是模糊的
Exception
依照这个原则,我们的样例应该被改成这样:
from requests.exceptions import RequestException
def save_website_title(url, filename):
try:
resp = requests.get(url)
except RequestException as e:
print(f'save failed: unable to get page content: {e}')
return False
# 这段正则操作本身就是不应该抛出异常的,所以我们没必要使用 try 语句块
# 假如 group 被误打成了 grop 也没关系,程序马上就会通过 AttributeError 来
# 告诉我们。
obj = re.search(r'(.*)', resp.text)
if not obj:
print('save failed: title tag not found in page content')
return False
title = obj.group(1)
try:
with open(filename, 'w') as fp:
fp.write(title)
except IOError as e:
print(f'save failed: unable to write to file {filename}: {e}')
return False
else:
return True
2. 别让异常破坏抽象一致性
大约四五年前,当时的我正在开发某移动应用的后端 API 项目。如果你也有过开发后端 API 的经验,那么你一定知道,这样的系统都需要制定一套“API 错误码规范”,来为客户端处理调用错误时提供方便。
一个错误码返回大概长这个样子:
// HTTP Status Code: 400
// Content-Type: application/json
{
"code": "UNABLE_TO_UPVOTE_YOUR_OWN_REPLY