03 Python 进阶(上):文件读写与模块系统 ✅04 Python 进阶(下):异常处理与综合实战(当前篇)Python 进阶(下):异常处理与综合实战
让程序在错误面前不再崩溃,用 try/except 构筑防御体系读完本篇你将能:理解 Python 异常的继承层级,用 try/except/else/finally 四段式结构捕获运行时错误,用 raise 主动抛出异常,编写自定义异常类,并完成一个健壮的 JSON 配置文件处理器——它能优雅处理文件缺失、格式错误、类型不匹配等各种意外情况。01 异常是什么:程序运行时的"警报系统"
上一篇你学会了用 open() 读取文件。但如果文件不存在呢?程序会直接崩溃,抛出一堆红色文字。这就是异常(Exception)——程序运行时遇到的意外情况。
异常不是 bug,而是程序运行环境中的"合理意外":用户可能输错了路径,网络可能突然断开,磁盘可能满了。好的程序不会假装这些不会发生,而是提前准备好应对方案。
没有异常处理的程序
先看一个会崩溃的例子——读取一个不存在的配置文件:
运行结果——Python 抛出了 FileNotFoundError:
Traceback (most recent call last): File "<stdin>", line 1, in <module>FileNotFoundError: [Errno 2] No such file or directory: 'config.json'这行报错信息包含三部分:错误类型(FileNotFoundError)、错误描述(No such file or directory)、涉及的文件路径(config.json)。Python 的异常信息非常详尽,学会阅读它就等于找到了问题的一半答案。
常见内置异常一览
Python 内置了大量异常类型,它们构成一棵继承树。所有异常的祖先都是 BaseException,日常 coding 最常遇到的是它的子类 Exception 的后代:
| | |
|---|
| | open("x.txt") |
| | int("abc") |
| | "a" + 1 |
| | d["missing"] |
| | [1,2][10] |
| | 1 / 0 |
| | json.loads("{bad") |
异常名的命名规律:都以 Error 或 Exception 结尾。看到这个名字就知道出了什么类型的问题。阅读报错信息时,先看最后一行的异常名,再看前面的调用栈定位发生位置。02 try/except:捕获异常的三种姿势
知道了异常是什么,接下来学习怎么"接住"它。Python 用 try/except 结构把可能出错的代码包起来——就像在施工区域拉起安全网,即使有人坠落也不会摔伤。
姿势一:捕获特定异常
最推荐的做法——只捕获你预期可能发生的异常类型,让其他意外错误继续向上传播:
| f = open("config.json", "r")except FileNotFoundError: |
try 块中的代码正常执行则跳过 except;如果抛出 FileNotFoundError 则执行 except 块;如果是其他异常(如 PermissionError),不会被这个 except 接住,程序依然会崩溃——这正是我们想要的,因为权限问题不应该被静默忽略。
姿势二:捕获并获取异常信息
用 as 关键字把异常对象赋值给变量,可以打印更详细的错误信息:
| print(f"错误类型:", type(e).__name__)# 转换失败: invalid literal for int() with base 10: 'hello' |
姿势三:同时捕获多种异常
一个 try 块可能遇到多种错误。你可以用多个 except 分支分别处理,也可以用一个元组捕获多种异常:
| withopen("data.json") as f:except FileNotFoundError:except json.JSONDecodeError as e: |
except: — 裸 except 会捕获所有异常(包括 KeyboardInterrupt、SystemExit),导致 Ctrl+C 都无法中断程序 ✓ 正确:except Exception: 只捕获普通异常,不拦截系统级退出信号 except 分支的匹配顺序是从上到下,一旦匹配就不再往下检查。因此子类异常必须写在父类异常前面,否则永远不会被执行。例如 JSONDecodeError 是 ValueError 的子类,如果先写 except ValueError,后面的 except JSONDecodeError 永远无法触发。
03 完整结构:else 和 finally
try/except 只是异常处理的基础骨架。Python 还提供了 else 和 finally 两个可选块,组成完整的四段式结构。你可以把 try 想象成一次手术:try 是手术过程,except 是处理并发症,else 是手术成功的后续治疗,finally 是无论结果如何都要打扫手术室。
四段式完整示例
| f = open("data.json", "r")except (FileNotFoundError, json.JSONDecodeError) as e:print(f"成功加载 {len(data)} 个键") |
else 块的作用是把"可能出错的代码"和"处理结果的代码"分开。如果把处理逻辑直接写在 try 块末尾,一旦处理逻辑本身出错也会被 except 捕获,导致错误被误判。else 块中的异常不会被同一个 except 捕获。04 主动抛出异常:raise
前面学的是"被动接住"异常,现在学习"主动制造"异常。当函数收到的参数不合法时,与其让程序带着错误数据继续跑出莫名其妙的结果,不如立刻用 raise 抛出异常,把问题扼杀在摇篮里。
基本用法
ifnotisinstance(age, int):raise TypeError(f"年龄必须是整数,得到:{type(age).__name__}")raise ValueError(f"年龄不合法:{age}")调用时的效果:
set_age("二十") # TypeError: 年龄必须是整数,得到:strset_age(-1) # ValueError: 年龄不合法:-1异常重新抛出
有时候你需要在 except 中记录日志,然后把异常继续往上抛,让调用者决定怎么处理。直接用不带参数的 raise 即可:
data = json.loads(raw_text)except json.JSONDecodeError:print(f"日志:JSON解析失败,原始内容:{raw_text[:50]}")raise "年龄不能为负数" — raise 只能抛出异常类或异常实例,不能抛出字符串 ✓ 正确:raise ValueError("年龄不能为负数")05 自定义异常:给错误起名字
内置异常是通用工具,但项目中的错误往往有特定含义。比如"配置文件缺少必填字段"和"网络请求超时"是完全不同的问题,用 ValueError 一把抓住会丢失语义。自定义异常就是给特定错误起一个专属名字,让调用者能精确捕获。
定义方式
自定义异常只需继承 Exception 类:
| classConfigError(Exception):classMissingFieldError(ConfigError):def__init__(self, field_name, config_path):self.field_name = field_nameself.config_path = config_pathsuper().__init__(f"配置文件 {config_path} 缺少必填字段:{field_name}") |
使用时,调用者可以选择精确捕获子类异常,也可以用基类一次性捕获整个家族:
| config = load_config("app.json")except MissingFieldError as e:print(f"缺少字段:{e.field_name}") config = get_default_config() config = get_default_config() |
自定义异常的命名规范:以 Error 或 Exception 结尾(如 DatabaseError、TimeoutException)。继承时先建一个基类(如 AppError),再派生子类,调用者可以按需选择捕获粒度——粗粒度捕获 AppError 处理所有应用异常,细粒度捕获子类分别处理。06 综合实战:健壮的配置文件处理器
前面学的所有知识——文件读写、JSON 解析、异常捕获、自定义异常、raise——将在这一节汇流成一个完整的实战项目。我们要写一个配置文件处理器,它能应对以下真实场景:配置文件不存在(返回默认值)、JSON 格式错误(提示修复)、必填字段缺失(精确报错)、字段类型错误(自动校验)。
完整代码
Python — config_handler.py | classConfigError(Exception):classMissingFieldError(ConfigError):def__init__(self, field, path):super().__init__(f"{path} 缺少字段:{field}")classFieldTypeError(ConfigError):def__init__(self, field, expected, got):f"字段 {field} 期望 {expected},得到 {got}")defload_config(path, required_fields=None): required_fields = required_fields or {}withopen(path, "r") as f:except FileNotFoundError:print(f"{path} 不存在,使用默认配置")except json.JSONDecodeError as e:raise ConfigError(f"{path} JSON格式错误:{e}")for field, expected_type in required_fields.items():raise MissingFieldError(field, path)ifnotisinstance(config[field], expected_type): field, expected_type.__name__,type(config[field]).__name__)defsave_config(path, config):withopen(path, "w") as f: json.dump(config, f, indent=2, ensure_ascii=False) |
使用示例
| required = {"host": str, "port": int} cfg = load_config("app.json", required)print(f"服务地址:{cfg['host']}:{cfg['port']}")except MissingFieldError as e: cfg = {"host": "127.0.0.1", "port": 8080}save_config("app.json", cfg) |
这个处理器的设计逻辑:load_config 函数内部用 try/except 处理文件层面的错误(不存在、格式错误),用 raise + 自定义异常处理业务层面的错误(字段缺失、类型不匹配)。调用者根据需要选择捕获粒度——MissingFieldError 精确处理缺字段(可以自动补默认值),ConfigError 兜底处理所有配置问题。save_config 不需要 try/except,因为写入失败属于系统级问题,应该让异常向上传播。
实际项目中的配置管理更复杂——通常还需要支持环境变量覆盖(如 os.getenv("PORT", 8080))、配置热重载、多环境配置分离(dev/prod)。但核心的异常处理思路与本例一致:文件层用 try/except,业务层用自定义异常 + raise。07 动手练习
编写一个函数 safe_divide(a, b),用 try/except 捕获除零错误,返回计算结果或错误提示字符串。测试:safe_divide(10, 2) 返回 5.0,safe_divide(10, 0) 返回 "错误:除数不能为零"。扩展第 06 节的配置处理器,增加一个 update_config(path, key, value) 函数:先加载现有配置(用 load_config),修改指定字段,再保存(用 save_config)。要求处理以下情况:配置文件不存在时自动创建、key 参数不是字符串时抛出 TypeError、保存失败时打印错误但不崩溃。设计一个简易的"重试装饰器" @retry(max_retries=3),被装饰的函数如果抛出异常会自动重试,最多重试 max_retries 次,全部失败后才抛出最后一次的异常。提示:需要了解 Python 装饰器语法(@函数名),结合 for 循环和 try/except 实现。思考:哪些异常值得重试(如网络超时),哪些不值得(如 ValueError)?异常继承层级try/except 捕获else/finallyraise 主动抛出自定义异常类异常重新抛出except 分支顺序JSON 配置处理器Python 的异常处理已掌握,下一篇切换到 C 语言,从指针开始理解内存地址、malloc/free 手动分配,以及内存泄漏的成因与检测。这是理解操作系统和底层安全的基础。