2. try-except语句:基本语法、捕获特定异常、捕获多个异常、异常链
异常处理,说白了就是给程序上「保险」。
我刚开始写自动化脚本那会儿,总觉得try-except是多余的。代码跑得好好的,干嘛要加这些?直到有一次,一个定时任务半夜挂了,整个运维群炸了锅。排查下来,就是一行文件读取没做异常处理——文件被临时锁住了,脚本直接崩溃。
从那以后,我写任何脚本,第一件事就是问自己:这里可能会出什么错?
2.1 基本语法:try-except的骨架
Python的异常处理,结构其实很简单。就三个关键字:try、except、finally(可选)。
try:
# 可能会出错的代码
result = 10 / 0
except:
# 出错后执行的代码
print("出错了!")
嗯,这里要注意:裸except会捕获所有异常。我个人不太建议这么写。为什么?因为你不知道到底出了什么错。可能是除零错误,可能是内存不足,甚至可能是键盘中断。统统吞掉,调试起来会非常痛苦。
更规范的写法,是带上异常类型:
try:
result = 10 / 0
except ZeroDivisionError:
print("除数不能为零!")
这样,你一眼就知道是哪里出了问题。
2.2 捕获特定异常:精准打击
不同的错误,处理方式完全不同。你想想看,文件不存在和权限不足,能一样处理吗?
Python内置了很多异常类型。我常用的几个:
| 异常类型 | 触发场景 | 我的处理习惯 |
|---|---|---|
| FileNotFoundError | 文件不存在 | 尝试创建或提示用户检查路径 |
| PermissionError | 权限不足 | 检查运行用户身份,或提权 |
| ValueError | 参数类型正确但值非法 | 打印具体值,方便定位 |
| KeyError | 字典键不存在 | 用get()方法替代直接访问 |
| TimeoutError | 操作超时 | 重试机制,最多3次 |
举个例子。我在做日志采集脚本时,经常要读取配置文件:
try:
with open("config.yaml", "r") as f:
config = yaml.safe_load(f)
except FileNotFoundError:
print("配置文件不存在,使用默认配置")
config = get_default_config()
except PermissionError:
print("权限不足,请以root用户运行")
sys.exit(1)
你看,文件不存在和权限不足,处理逻辑完全不同。前者可以降级,后者必须退出。
2.3 捕获多个异常:一网打尽
有时候,一段代码可能抛出多种异常。你当然可以写多个except块。但更优雅的方式,是合并同类项。
Python支持两种写法:
写法一:多个except块
try:
data = fetch_from_api()
result = process_data(data)
except ConnectionError:
print("网络连接失败")
except TimeoutError:
print("请求超时")
except ValueError:
print("数据格式异常")
这种写法,每个异常独立处理。适合异常之间差异很大的场景。
写法二:元组捕获
try:
data = fetch_from_api()
result = process_data(data)
except (ConnectionError, TimeoutError) as e:
print(f"网络相关异常:{e}")
# 重试逻辑
retry_request()
except ValueError as e:
print(f"数据异常:{e}")
# 记录错误数据
log_bad_data(data)
我个人更推荐第二种。为什么?因为ConnectionError和TimeoutError本质上都是网络问题,处理逻辑是一样的——重试。没必要写两遍。
2.4 异常链:追根溯源
这个知识点,很多人会忽略。但我觉得,它恰恰是生产环境调试的利器。
什么叫异常链?就是异常A导致异常B,你希望同时看到A和B的信息。
举个例子。我在做数据库迁移脚本时,遇到过这种情况:
try:
with open("migration.sql", "r") as f:
sql = f.read()
except FileNotFoundError as e:
raise RuntimeError("无法执行迁移,缺少SQL文件") from e
这里,from e就是异常链。当RuntimeError被抛出时,Python会同时显示原始异常FileNotFoundError的信息。
输出效果是这样的:
RuntimeError: 无法执行迁移,缺少SQL文件
The above exception was the direct cause of the following exception:
FileNotFoundError: [Errno 2] No such file or directory: 'migration.sql'
你看,一眼就能看出:是文件找不到,导致了迁移失败。而不是笼统的「迁移失败」。
什么时候用异常链?
- 当你把底层异常包装成业务异常时
- 当你需要保留完整的错误上下文时
- 当你希望调试者能快速定位根因时
什么时候不用?
- 当你明确知道原始异常无关紧要时
- 当你不想暴露内部实现细节时
2.5 实战建议:我的异常处理三板斧
说了这么多,总结一下我个人的经验:
- 先想后写:写代码前,先想清楚这段代码可能出什么错。列出所有可能的异常类型。
- 分级处理:可恢复的异常(如网络超时)做重试;不可恢复的异常(如配置错误)直接退出并报警。
- 日志先行:每个except块里,第一行必须是写日志。哪怕你后面什么都不做,日志也得留下。
嗯,最后说一句。异常处理不是为了掩盖错误,而是为了优雅地处理错误。好的异常处理,能让你的脚本在凌晨三点出问题时,自动降级、自动重试、自动报警,而不是直接崩溃。
这才是自动化运维该有的样子。