上一篇你学会了用 open() 读取文件。但如果文件不存在呢?程序会直接崩溃,抛出一堆红色文字。这就是异常(Exception)——程序运行时遇到的意外情况。
异常不是 bug,而是程序运行环境中的"合理意外":用户可能输错了路径,网络可能突然断开,磁盘可能满了。好的程序不会假装这些不会发生,而是提前准备好应对方案。
先看一个会崩溃的例子——读取一个不存在的配置文件:
运行结果——Python 抛出了 FileNotFoundError:
这行报错信息包含三部分:错误类型(FileNotFoundError)、错误描述(No such file or directory)、涉及的文件路径(config.json)。Python 的异常信息非常详尽,学会阅读它就等于找到了问题的一半答案。
Python 内置了大量异常类型,它们构成一棵继承树。所有异常的祖先都是 BaseException,日常 coding 最常遇到的是它的子类 Exception 的后代:
| 异常名 | 触发场景 | 示例代码 |
|---|---|---|
| FileNotFoundError | 打开不存在的文件 | open("x.txt") |
| ValueError | 值类型正确但内容非法 | int("abc") |
| TypeError | 对错误类型执行操作 | "a" + 1 |
| KeyError | 字典中找不到键 | d["missing"] |
| IndexError | 列表索引越界 | [1,2][10] |
| ZeroDivisionError | 除数为零 | 1 / 0 |
| json.JSONDecodeError | JSON 格式解析失败 | json.loads("{bad") |
知道了异常是什么,接下来学习怎么"接住"它。Python 用 try/except 结构把可能出错的代码包起来——就像在施工区域拉起安全网,即使有人坠落也不会摔伤。
最推荐的做法——只捕获你预期可能发生的异常类型,让其他意外错误继续向上传播:
try 块中的代码正常执行则跳过 except;如果抛出 FileNotFoundError 则执行 except 块;如果是其他异常(如 PermissionError),不会被这个 except 接住,程序依然会崩溃——这正是我们想要的,因为权限问题不应该被静默忽略。
用 as 关键字把异常对象赋值给变量,可以打印更详细的错误信息:
一个 try 块可能遇到多种错误。你可以用多个 except 分支分别处理,也可以用一个元组捕获多种异常:
except Exception: 只捕获普通异常,不拦截系统级退出信号
except 分支的匹配顺序是从上到下,一旦匹配就不再往下检查。因此子类异常必须写在父类异常前面,否则永远不会被执行。例如 JSONDecodeError 是 ValueError 的子类,如果先写 except ValueError,后面的 except JSONDecodeError 永远无法触发。
try/except 只是异常处理的基础骨架。Python 还提供了 else 和 finally 两个可选块,组成完整的四段式结构。你可以把 try 想象成一次手术:try 是手术过程,except 是处理并发症,else 是手术成功的后续治疗,finally 是无论结果如何都要打扫手术室。
| 块 | 执行时机 | 是否可选 |
|---|---|---|
| try | 始终执行 | 必需 |
| except | try 块抛出异常时 | 至少一个(或用 else) |
| else | try 块未抛出异常时 | 可选 |
| finally | 无论如何都执行 | 可选 |
前面学的是"被动接住"异常,现在学习"主动制造"异常。当函数收到的参数不合法时,与其让程序带着错误数据继续跑出莫名其妙的结果,不如立刻用 raise 抛出异常,把问题扼杀在摇篮里。
调用时的效果:
有时候你需要在 except 中记录日志,然后把异常继续往上抛,让调用者决定怎么处理。直接用不带参数的 raise 即可:
raise ValueError("年龄不能为负数")
内置异常是通用工具,但项目中的错误往往有特定含义。比如"配置文件缺少必填字段"和"网络请求超时"是完全不同的问题,用 ValueError 一把抓住会丢失语义。自定义异常就是给特定错误起一个专属名字,让调用者能精确捕获。
自定义异常只需继承 Exception 类:
使用时,调用者可以选择精确捕获子类异常,也可以用基类一次性捕获整个家族:
前面学的所有知识——文件读写、JSON 解析、异常捕获、自定义异常、raise——将在这一节汇流成一个完整的实战项目。我们要写一个配置文件处理器,它能应对以下真实场景:配置文件不存在(返回默认值)、JSON 格式错误(提示修复)、必填字段缺失(精确报错)、字段类型错误(自动校验)。
这个处理器的设计逻辑:load_config 函数内部用 try/except 处理文件层面的错误(不存在、格式错误),用 raise + 自定义异常处理业务层面的错误(字段缺失、类型不匹配)。调用者根据需要选择捕获粒度——MissingFieldError 精确处理缺字段(可以自动补默认值),ConfigError 兜底处理所有配置问题。save_config 不需要 try/except,因为写入失败属于系统级问题,应该让异常向上传播。
os.getenv("PORT", 8080))、配置热重载、多环境配置分离(dev/prod)。但核心的异常处理思路与本例一致:文件层用 try/except,业务层用自定义异常 + raise。safe_divide(a, b),用 try/except 捕获除零错误,返回计算结果或错误提示字符串。测试:safe_divide(10, 2) 返回 5.0,safe_divide(10, 0) 返回 "错误:除数不能为零"。update_config(path, key, value) 函数:先加载现有配置(用 load_config),修改指定字段,再保存(用 save_config)。要求处理以下情况:配置文件不存在时自动创建、key 参数不是字符串时抛出 TypeError、保存失败时打印错误但不崩溃。@retry(max_retries=3),被装饰的函数如果抛出异常会自动重试,最多重试 max_retries 次,全部失败后才抛出最后一次的异常。提示:需要了解 Python 装饰器语法(@函数名),结合 for 循环和 try/except 实现。思考:哪些异常值得重试(如网络超时),哪些不值得(如 ValueError)?