2. try-except语句:基本语法、捕获特定异常、捕获多个异常、异常链

异常处理,说白了就是给程序上「保险」。

我刚开始写自动化脚本那会儿,总觉得try-except是多余的。代码跑得好好的,干嘛要加这些?直到有一次,一个定时任务半夜挂了,整个运维群炸了锅。排查下来,就是一行文件读取没做异常处理——文件被临时锁住了,脚本直接崩溃。

从那以后,我写任何脚本,第一件事就是问自己:这里可能会出什么错?

2.1 基本语法:try-except的骨架

Python的异常处理,结构其实很简单。就三个关键字:try、except、finally(可选)。

try:
    # 可能会出错的代码
    result = 10 / 0
except:
    # 出错后执行的代码
    print("出错了!")

嗯,这里要注意:裸except会捕获所有异常。我个人不太建议这么写。为什么?因为你不知道到底出了什么错。可能是除零错误,可能是内存不足,甚至可能是键盘中断。统统吞掉,调试起来会非常痛苦。

避坑指南:我曾经接手过一个遗留脚本,里面全是裸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本质上都是网络问题,处理逻辑是一样的——重试。没必要写两遍。

小技巧:用as e捕获异常对象,可以拿到具体的错误信息。这在写日志时特别有用。我习惯在每条日志里都带上异常详情,方便事后复盘。

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 实战建议:我的异常处理三板斧

说了这么多,总结一下我个人的经验:

  1. 先想后写:写代码前,先想清楚这段代码可能出什么错。列出所有可能的异常类型。
  2. 分级处理:可恢复的异常(如网络超时)做重试;不可恢复的异常(如配置错误)直接退出并报警。
  3. 日志先行:每个except块里,第一行必须是写日志。哪怕你后面什么都不做,日志也得留下。

嗯,最后说一句。异常处理不是为了掩盖错误,而是为了优雅地处理错误。好的异常处理,能让你的脚本在凌晨三点出问题时,自动降级、自动重试、自动报警,而不是直接崩溃。

这才是自动化运维该有的样子。