序言
Python 里的函数可以返回多个值。基于这个能力我们可以让函数:同时返回结果与错误信息的函数。但是在 Python 世界里,这并非解决此类问题的最佳办法。因为这种做法会增加调用方进行错误处理的成本,尤其是当很多函数都遵循这 个规范而且存在多层调用时。Python 具备完善的异常(Exception)机制(官方文档关于 EAFP 的说明),并且在某种程 度上鼓励我们使用异常(可参考“Write Cleaner Python: Use Exceptions”), 所以使用异常来进行错误流程处理才是更地道的做法。
异常处理介绍
Python的异常处理有一些使用技巧如下:
处理多个异常: 有一个代码片段可能会抛出多个不同的异常,怎样才能不创建大量重复代码就能处理所有的可能异 常呢?
- 用单个代码块处理不同的异常,将所有异常放入一个元组中
try:
client_obj.get_url(url)
except (URLError, ValueError, SocketTimeout):
client_obj.remove_url(url)
- 想对其中某个异常进行不同的处理,可以将其放入另外一个 except 语句中
try:
client_obj.get_url(url)
except (URLError, ValueError):
client_obj.remove_url(url)
except SocketTimeout:
client_obj.handle_url_timeout(url)
- 很多的异常会有层级关系,对于这种情况,可以使用它们的一个基类来捕获所有的异常
try:
f = open(filename)
except (FileNotFoundError, PermissionError):
pass
# 重写为
try:
f = open(filename)
# OSError 是 FileNotFoundError 和 PermissionError 异常的基类
except OSError:
pass
创建自定义异常:在程序中引入自定义异常可以使得你的代码更具可读性,它可以让程序捕获一个范围很窄的特定异常。
# 自定义异常类应该总是继承自内置的 Exception 类,或者是继承自那些本身就是从 Exception 继承而来的类
class NetworkError(Exception):
pass
class HostnameError(NetworkError):
pass
class TimeoutError(NetworkError):
pass
class ProtocolError(NetworkError):
pass
捕获异常后抛出异常 :
- 你想捕获一个异常后抛出另外一个不同的异常,同时还得在异常回溯中保留两个异常的信息,使用 raise from 。
>>> def example():
... try:
... int('N/A')
... except ValueError as e:
# 使用 raise from 语句来代替简单的 raise 语句。它会让你同时保留两个异常的信息
... raise RuntimeError('A parsing error occurred') from e
>>> example()
Traceback (most recent call last):
File "<input>", line 3, in example
ValueError: invalid literal for int() with base 10: 'N/A'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 5, in example
RuntimeError: A parsing error occurred
大多数情况下, raise 语句都应该被改成 raise from 语句。这样做的原因是应该显示的将原因链接起来。也 就是说, RuntimeError 是直接从 ValueError 衍生而来。这种关系可以从回溯结果中看出来。
- 如果想忽略掉异常链,可使用 raise from None
>>> def example3():
... try:
... int('N/A')
... except ValueError:
... raise RuntimeError('A parsing error occurred') from None
... >>>example3()
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 5, in example RuntimeError: A parsing error occurred
- 重新抛出被捕获的异常: 简单的使用一个单独的 rasie 语句即可
>>> def example():
... try:
... int('N/A')
... except ValueError:
... print("Didn't work")
... raise
... >>> example()
Didn't work
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 3, in example
ValueError: invalid literal for int() with base 10: 'N/A' >>>
Code Review 中发现的问题
异常捕捉内容过多 : 很多代码直接捕捉 Exception ,例如
try:
do_something()
except Exception as e:
pass
用一段代码作为样例
# -*- 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()
这一串代码无论如何修改网址和目标文件的值,程序仍然会报错 “save failed: unable to...” , 问题就藏在这个硕大无 比的 try ... except 语句块里,因为代码中犯了一个 小错误 ,获取正则匹配串的方法错打成了 obj.grop(1) ,少了一个 'u'( obj.group(1) ),但正是因为那个过于庞大、含糊的异常捕获,这个由打错方法名导致的原本该被抛出的 AttibuteError 却被吞噬; 修改后的代码:
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
异常捕捉的建议
- 只捕获可能会抛出异常的语句,避免含糊的捕获逻辑;
- 在程序中引入自定义异常增加代码可读性,让程序捕获一个范围很窄的特定异常; 保持模块异常类的抽象一致性,必要时对底层异常类进行包装 ;
- 需要问题回溯的时候, 可以使用 raise from 语句来代替简单的 raise 语句。