Appearance
【Python 工匠】第六章 - 异常处理的三个好习惯,阅读笔记
作者总结了三个Python异常处理的最佳实践。
1. 只做最精确的异常捕获
应当摒弃"异常是有害的,好的程序应该捕获所有异常以确保平稳运行"这一错误的编程理念。异常处理的核心目标并非是无差别地捕获所有异常,而是应该专注于那些确实可能抛出异常的特定代码块,并且要尽可能使用具体的异常类型,而不是模糊地使用 Exception 类。
Bad Case:
python
# -*- 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'<title>(.*)</title>', 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()
Good Practise:
python
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'<title>(.*)</title>', 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. 别让异常破坏抽象一致性
Bad Case:
python
# 在某个处理图像的模块内部
# <PROJECT_ROOT>/util/image/processor.py
def process_image(...):
try:
image = Image.open(fp)
except Exception:
# 说明(非项目原注释):该异常将会被 Django 的中间件捕获,往前端返回
# "上传的图片格式有误" 信息
raise error_codes.INVALID_IMAGE_UPLOADED
... ...
在处理用户上传图片的 POST 请求时,process_image 函数会尝试解析文件对象。如果该文件无法作为图片正常打开,函数会抛出 error_codes.INVALID_IMAGE_UPLOADED (APIErrorCode 的子类) 异常,并向调用方返回错误代码 JSON。然而,当你试图在离线脚本中复用这个方法时,会面临两个技术限制:
- 你必须捕获一个名为 INVALID_IMAGE_UPLOADED 的异常,即使你处理的图片并非来自用户上传
- 你必须引入 APIErrorCode 异常类作为依赖来捕获异常,即使你的脚本与 Django API 完全无关
这个设计问题反映了异常处理机制中的层级混乱。APIErrorCode 异常类原本设计用于表达面向终端用户的、可识别的"错误代码",它在项目架构中处于最高层。然而,出于实现便捷的考虑,我们在底层模块中直接使用了这个高层异常。这种跨层级的异常处理方式,影响了 image.processor 模块的独立性,使其难以在其他场景中复用。
要解决"模块抛出了高于所属抽象层级的异常"的问题,我们可以采取以下方案:
- 确保模块只抛出与其抽象层级相符的异常。例如,image.processer 模块应该抛出自己封装的 ImageOpenError 异常
- 在适当的位置进行异常转换。比如,应该在更接近高层抽象的地方 (如视图 View 函数),将底层的 ImageOpenError 异常转换为高层的 APIErrorCode 异常
同时,我们也要注意避免在高层代码中暴露底层异常。例如,不应该让 Web API 直接向用户返回底层文件系统的异常信息,而是应该将其转换为合适的高层业务异常。这样可以保持各层级的职责清晰,提高代码的可维护性。
Good Practise:
python
# <PROJECT_ROOT>/util/image/processor.py
class ImageOpenError(Exception):
pass
def process_image(...):
try:
image = Image.open(fp)
except Exception as e:
raise ImageOpenError(exc=e)
... ...
# <PROJECT_ROOT>/app/views.py
def foo_view_function(request):
try:
process_image(fp)
except ImageOpenError:
raise error_codes.INVALID_IMAGE_UPLOADED
3. 异常处理不应该喧宾夺主
当代码中充斥着大量的异常处理语句 (try、except、raise) 时,往往会模糊代码的主要业务逻辑。这种情况在处理文件操作、网络请求等容易出错的场景中尤为常见。
Bad Case:
python
def upload_avatar(request):
"""用户上传新头像"""
try:
avatar_file = request.FILES['avatar']
except KeyError:
raise error_codes.AVATAR_FILE_NOT_PROVIDED
try:
resized_avatar_file = resize_avatar(avatar_file)
except FileTooLargeError as e:
raise error_codes.AVATAR_FILE_TOO_LARGE
except ResizeAvatarError as e:
raise error_codes.AVATAR_FILE_INVALID
try:
request.user.avatar = resized_avatar_file
request.user.save()
except Exception:
raise error_codes.INTERNAL_SERVER_ERROR
return HttpResponse({})
Python 的 上下文管理器 (Context Manager) 提供了一种优雅的解决方案。通过使用 with 语句,我们可以将异常处理逻辑从核心业务代码中分离出来。
Good Practise:
python
class raise_api_error:
"""captures specified exception and raise ApiErrorCode instead
:raises: AttributeError if code_name is not valid
"""
def __init__(self, captures, code_name):
self.captures = captures
self.code = getattr(error_codes, code_name)
def __enter__(self):
# 该方法将在进入上下文时调用
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# 该方法将在退出上下文时调用
# exc_type, exc_val, exc_tb 分别表示该上下文内抛出的
# 异常类型、异常值、错误栈
if exc_type is None:
return False
if exc_type == self.captures:
raise self.code from exc_val
return False
def upload_avatar(request):
"""用户上传新头像"""
with raise_api_error(KeyError, 'AVATAR_FILE_NOT_PROVIDED'):
avatar_file = request.FILES['avatar']
with raise_api_error(ResizeAvatarError, 'AVATAR_FILE_INVALID'),\
raise_api_error(FileTooLargeError, 'AVATAR_FILE_TOO_LARGE'):
resized_avatar_file = resize_avatar(avatar_file)
with raise_api_error(Exception, 'INTERNAL_SERVER_ERROR'):
request.user.avatar = resized_avatar_file
request.user.save()
return HttpResponse({})
这种方式不仅让代码更加清晰易读,还能确保资源的正确释放和异常的统一处理。上下文管理器特别适合处理需要配对操作的场景,如文件操作、数据库连接、锁的获取与释放等。