2. try-except语句:基本语法、捕获单个异常、捕获多个异常、异常链
异常处理,说白了就是给代码上「保险」。
我刚开始写Python那会儿,总觉得try-except是多余的。代码跑得好好的,干嘛要加这些?直到有一次,我写了一个爬虫程序,跑了整整两天,结果因为一个网络超时,整个程序直接崩了。嗯,从那以后,我再也不敢不写异常处理了。
2.1 基本语法
try-except的结构其实很简单。你想想看,就是「试试看,不行就拉倒」的逻辑。
try:
# 可能会出错的代码
result = 10 / 0
except:
# 出错后执行的代码
print("出错了!")
我个人习惯把try块写得尽量小。为什么?因为范围越大,越难定位问题。你总不能把整个函数都塞进try里吧?
核心要点:
- try块:放可能引发异常的代码
- except块:异常发生时的处理逻辑
- 如果没异常,except块不会执行
2.2 捕获单个异常
光知道「出错了」还不够。你得知道出了什么错。
try:
number = int(input("请输入一个数字: "))
result = 100 / number
except ValueError:
print("你输入的不是数字!")
except ZeroDivisionError:
print("不能除以零!")
我在项目中遇到过这种情况:用户输入了一个空字符串,int()直接抛ValueError。如果不指定异常类型,你根本不知道是输入问题还是计算问题。
我的建议:永远指定具体的异常类型。裸except就像闭着眼睛抓蚊子——你可能抓到了,但不知道抓的是什么。
2.3 捕获多个异常
有时候,不同的异常需要同样的处理方式。这时候可以合并捕获。
try:
data = open("config.txt").read()
value = int(data)
except (FileNotFoundError, ValueError) as e:
print(f"读取配置失败: {e}")
# 使用默认值
value = 0
你看,文件不存在和内容格式不对,都是配置读取失败。把它们放一起处理,代码清爽多了。
还有一种写法,用元组把异常类型包起来:
try:
# 一些操作
pass
except (TypeError, ValueError, IndexError):
print("常见的几种错误,统一处理")
注意:别把不相关的异常放一起。比如把KeyboardInterrupt和ValueError放一起,用户按Ctrl+C想退出程序,结果被你当成普通错误处理了——这就不对了。
2.4 异常链
异常链是个好东西,但很多人不会用。
什么叫异常链?就是一个异常引发了另一个异常。比如你读取文件时遇到编码错误,然后你想抛出一个更上层的「配置加载失败」异常。这时候,原始异常不能丢。
def load_config():
try:
with open("config.json", "r") as f:
return json.load(f)
except FileNotFoundError as e:
raise RuntimeError("配置加载失败") from e
try:
config = load_config()
except RuntimeError as e:
print(f"错误: {e}")
print(f"原始原因: {e.__cause__}")
我曾经调试过一个线上问题,日志里只看到「数据库连接失败」,但根本不知道是网络问题还是认证问题。后来加了异常链,才发现是DNS解析超时。嗯,从那以后,我所有自定义异常都带上了原始异常。
Python 3里,异常链有两种方式:
| 方式 | 语法 | 效果 |
|---|---|---|
| 隐式链 | except块中直接raise新异常 | 自动保留__context__ |
| 显式链 | raise ... from ... | 明确指定__cause__ |
我的经验:能用显式链就别用隐式链。显式链让代码意图更清晰,别人看代码时一眼就知道「哦,这个异常是由那个异常引起的」。
2.5 知识体系图
一个小技巧:调试时可以用traceback模块打印完整的异常链。这样你就能看到异常从哪来、到哪去,整个调用路径一目了然。
好了,try-except这块儿就这些。记住一个原则:异常处理不是用来掩盖错误的,而是用来优雅地处理错误的。别把try-except当成万能胶,哪儿都糊一层——那样只会让问题更难排查。
我曾经踩过的坑:在except块里又写了可能出错的代码,结果异常没处理好,反而引发了新的异常。记住,except块里的代码也要小心,最好只做简单的日志记录和资源清理。