写代码时遇到错误很正常。很多人习惯用一个大的 try...except Exception 把所有错误包起来。这种做法很省事。程序抓到了异常,打印一句“出错了”,然后继续跑。看起来没问题,实际上埋了不少坑。
我见过一个同事写的脚本。他用了裸的 except:,没有指定任何异常类型。有一天脚本突然在凌晨挂了。他跑过去看日志,只看到一行“程序异常,请联系管理员”。具体是什么错?不知道。是网络断了,是文件没找到,还是内存不够?全要重跑一遍才能排查。他那天加班到天亮。
这个例子说明,粗糙的异常捕获等于没捕获。你只知道出事了,但不知道出了什么事。
细化except块并不难。你要先搞清楚这段代码可能会遇到哪些问题。比如打开一个文件,可能文件不存在,可能没有权限,可能磁盘满了。每个错误都是不同的。用 try: open('data.txt') except FileNotFoundError: 这样写,你知道是文件不见了。再补一个 except PermissionError:,就能区分开是权限问题。你还可以加个 except OSError as e: 兜底,记录下具体错误码。
有人担心写太多except块会让代码变长。确实会多几行。但比起半夜爬起来解决问题,这几行代码很划算。你每多写一个具体的异常类型,就等于提前给自己留了一条线索。错误信息越具体,修复速度越快。
还有一个细节要注意。不同异常要有不同处理方式。文件不存在就去创建它。网络超时就重试一次。内存不足就提示用户关闭其他程序。如果你把所有异常混在一起处理,统一的“出错了”会让用户不知道该怎么办。用户会反复点击重试按钮,或者直接删掉你的程序。
我自己的习惯是,每个except块只做一件事。要么记录日志,要么给用户反馈,要么尝试自动修复。不会在一个块里既写日志又发邮件还尝试重试。混在一起容易出新的错误,旧错没处理好新错又来了。
你可以在代码里这样写。假设你写了一个爬数据的函数:
try:
data = fetch_data(url)
except requests.ConnectionError:
print('网络断了,检查你的网络连接。')
except requests.Timeout:
print('服务器响应太久,3秒后重试。')
time.sleep(3)
data = fetch_data(url)
except Exception as e:
log_file.write(f'未知错误: {e}')
raise
这样写,用户看到的是人话。你自己看日志也知道问题出在哪。最后那个 except Exception 是兜底的,但加了 raise 又把错误抛出去,不会掩盖问题。
有些人觉得异常捕获不重要,只要程序不崩溃就行。这个想法很危险。程序不崩只是表象。你丢失了数据,错失了商机,得罪了用户,这些损失比程序崩溃大得多。一个健壮的程序,不是不出错,而是出了错能告诉你错在哪里。
下次你再写 try except 的时候,多花一分钟想想。这段代码可能遇到什么异常?每个异常该怎么回应?按这个思路写,你的代码会可靠很多。你也会少熬夜。我这些话糙理不糙,你试一试就知道了。