📚 全栈开发学习系列
01 路线总览与环境搭建 ✅
02 Python 语法基础 ✅
03 Python 进阶(上):文件读写与模块系统 ✅
04 Python 进阶(下):异常处理与综合实战(当前篇)
05 C 语言与内存:指针、分配与释放
06 数据结构与算法:链表、栈与队列
07 Git 版本控制:分支、合并与冲突解决
08 Linux 命令行:文件、权限与进程管理

Python 进阶(下):异常处理与综合实战

让程序在错误面前不再崩溃,用 try/except 构筑防御体系
进阶篇 异常处理 综合实战
读完本篇你将能:理解 Python 异常的继承层级,用 try/except/else/finally 四段式结构捕获运行时错误,用 raise 主动抛出异常,编写自定义异常类,并完成一个健壮的 JSON 配置文件处理器——它能优雅处理文件缺失、格式错误、类型不匹配等各种意外情况。
📑 本文目录
01异常是什么:程序运行时的"警报系统"
02try/except:捕获异常的三种姿势
03完整结构:else 和 finally
04主动抛出异常:raise
05自定义异常:给错误起名字
06综合实战:健壮的配置文件处理器
07动手练习

01 异常是什么:程序运行时的"警报系统"

上一篇你学会了用 open() 读取文件。但如果文件不存在呢?程序会直接崩溃,抛出一堆红色文字。这就是异常(Exception)——程序运行时遇到的意外情况。

异常不是 bug,而是程序运行环境中的"合理意外":用户可能输错了路径,网络可能突然断开,磁盘可能满了。好的程序不会假装这些不会发生,而是提前准备好应对方案。

没有异常处理的程序

先看一个会崩溃的例子——读取一个不存在的配置文件:

Python
open("config.json", "r")

运行结果——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 的后代:

异常名 触发场景 示例代码
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")
💡 小贴士
异常名的命名规律:都以 Error 或 Exception 结尾。看到这个名字就知道出了什么类型的问题。阅读报错信息时,先看最后一行的异常名,再看前面的调用栈定位发生位置。

02 try/except:捕获异常的三种姿势

知道了异常是什么,接下来学习怎么"接住"它。Python 用 try/except 结构把可能出错的代码包起来——就像在施工区域拉起安全网,即使有人坠落也不会摔伤。

姿势一:捕获特定异常

最推荐的做法——只捕获你预期可能发生的异常类型,让其他意外错误继续向上传播:

Python
1
2
3
4
5
6
7
8
9
try:
    f = open("config.json", "r")
    content = f.read()
    f.close()
    print(content)
except FileNotFoundError:
    print("配置文件不存在,使用默认配置")
# 输出:配置文件不存在,使用默认配置

try 块中的代码正常执行则跳过 except;如果抛出 FileNotFoundError 则执行 except 块;如果是其他异常(如 PermissionError),不会被这个 except 接住,程序依然会崩溃——这正是我们想要的,因为权限问题不应该被静默忽略。

姿势二:捕获并获取异常信息

用 as 关键字把异常对象赋值给变量,可以打印更详细的错误信息:

Python
1
2
3
4
5
6
7
8
try:
    num = int("hello")
except ValueError as e:
    print(f"转换失败:", e)
    print(f"错误类型:", type(e).__name__)
# 输出:
# 转换失败: invalid literal for int() with base 10: 'hello'

姿势三:同时捕获多种异常

一个 try 块可能遇到多种错误。你可以用多个 except 分支分别处理,也可以用一个元组捕获多种异常:

Python
1
2
3
4
5
6
7
8
9
10
11
12
import json
try:
    with open("data.json") as f:
        data = json.load(f)
except FileNotFoundError:
    print("文件不存在")
    data = {}
except json.JSONDecodeError as e:
    print(f"JSON 格式错误:", e)
    data = {}
print(data)
⚠️ 常见错误
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 是无论结果如何都要打扫手术室。

try
执行代码
→
except
出错时处理
→
else
成功后执行
→
finally
必做清理

四段式完整示例

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import json
try:
    f = open("data.json", "r")
    data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
    print(f"加载失败:{e}")
    data = {}
else:
    print(f"成功加载 {len(data)} 个键")
    f.close()  # 成功时关闭文件
finally:
    print("操作结束")
# 输出:操作结束
块 执行时机 是否可选
try 始终执行 必需
except try 块抛出异常时 至少一个(或用 else)
else try 块未抛出异常时 可选
finally 无论如何都执行 可选
💡 小贴士
else 块的作用是把"可能出错的代码"和"处理结果的代码"分开。如果把处理逻辑直接写在 try 块末尾,一旦处理逻辑本身出错也会被 except 捕获,导致错误被误判。else 块中的异常不会被同一个 except 捕获。

04 主动抛出异常:raise

前面学的是"被动接住"异常,现在学习"主动制造"异常。当函数收到的参数不合法时,与其让程序带着错误数据继续跑出莫名其妙的结果,不如立刻用 raise 抛出异常,把问题扼杀在摇篮里。

基本用法

Python
def set_age(age):
    if not isinstance(age, int):
        raise TypeError(f"年龄必须是整数,得到:{type(age).__name__}")
    if age < 0 or age > 150:
        raise ValueError(f"年龄不合法:{age}")
    return age

调用时的效果:

Python
set_age(25)    # 正常返回 25
set_age("二十")  # TypeError: 年龄必须是整数,得到:str
set_age(-1)    # ValueError: 年龄不合法:-1

异常重新抛出

有时候你需要在 except 中记录日志,然后把异常继续往上抛,让调用者决定怎么处理。直接用不带参数的 raise 即可:

Python
try:
    data = json.loads(raw_text)
except json.JSONDecodeError:
    print(f"日志:JSON解析失败,原始内容:{raw_text[:50]}")
    raise  # 重新抛出当前异常
⚠️ 常见错误
raise "年龄不能为负数" — raise 只能抛出异常类或异常实例,不能抛出字符串
✓ 正确:raise ValueError("年龄不能为负数")

05 自定义异常:给错误起名字

内置异常是通用工具,但项目中的错误往往有特定含义。比如"配置文件缺少必填字段"和"网络请求超时"是完全不同的问题,用 ValueError 一把抓住会丢失语义。自定义异常就是给特定错误起一个专属名字,让调用者能精确捕获。

定义方式

自定义异常只需继承 Exception 类:

Python
1
2
3
4
5
6
7
8
9
10
class ConfigError(Exception):
    """配置文件相关的异常基类"""
    pass
class MissingFieldError(ConfigError):
    """缺少必填字段"""
    def __init__(self, field_name, config_path):
        self.field_name = field_name
        self.config_path = config_path
        super().__init__(f"配置文件 {config_path} 缺少必填字段:{field_name}")

使用时,调用者可以选择精确捕获子类异常,也可以用基类一次性捕获整个家族:

Python
1
2
3
4
5
6
7
8
9
try:
    config = load_config("app.json")
except MissingFieldError as e:
    print(f"缺少字段:{e.field_name}")
    config = get_default_config()
except ConfigError as e:
    print(f"其他配置错误:{e}")
    config = get_default_config()
💡 小贴士
自定义异常的命名规范:以 Error 或 Exception 结尾(如 DatabaseError、TimeoutException)。继承时先建一个基类(如 AppError),再派生子类,调用者可以按需选择捕获粒度——粗粒度捕获 AppError 处理所有应用异常,细粒度捕获子类分别处理。

06 综合实战:健壮的配置文件处理器

前面学的所有知识——文件读写、JSON 解析、异常捕获、自定义异常、raise——将在这一节汇流成一个完整的实战项目。我们要写一个配置文件处理器,它能应对以下真实场景:配置文件不存在(返回默认值)、JSON 格式错误(提示修复)、必填字段缺失(精确报错)、字段类型错误(自动校验)。

完整代码

Python — config_handler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import json
import os
class ConfigError(Exception):
    """配置处理异常基类"""
    pass
class MissingFieldError(ConfigError):
    """必填字段缺失"""
    def __init__(self, field, path):
        super().__init__(f"{path} 缺少字段:{field}")
class FieldTypeError(ConfigError):
    """字段类型不匹配"""
    def __init__(self, field, expected, got):
        super().__init__(
            f"字段 {field} 期望 {expected},得到 {got}")
def load_config(path, required_fields=None):
    """加载并校验JSON配置文件"""
    required_fields = required_fields or {}
    try:
        with open(path, "r") as f:
            config = json.load(f)
    except FileNotFoundError:
        print(f"{path} 不存在,使用默认配置")
        return {}
    except json.JSONDecodeError as e:
        raise ConfigError(f"{path} JSON格式错误:{e}")
    for field, expected_type in required_fields.items():
        if field not in config:
            raise MissingFieldError(field, path)
        if not isinstance(config[field], expected_type):
            raise FieldTypeError(
                field, expected_type.__name__,
                type(config[field]).__name__)
    return config
def save_config(path, config):
    """保存配置到JSON文件"""
    with open(path, "w") as f:
        json.dump(config, f, indent=2, ensure_ascii=False)
    print(f"配置已保存到 {path}")

使用示例

Python
1
2
3
4
5
6
7
8
9
10
11
12
required = {"host": str, "port": int}
try:
    cfg = load_config("app.json", required)
    print(f"服务地址:{cfg['host']}:{cfg['port']}")
except MissingFieldError as e:
    print(f"需要补充配置:{e}")
    cfg = {"host": "127.0.0.1", "port": 8080}
    save_config("app.json", cfg)
except ConfigError as e:
    print(f"配置错误:{e}")
    raise

这个处理器的设计逻辑: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/finally raise 主动抛出 自定义异常类 异常重新抛出 except 分支顺序 JSON 配置处理器
下篇预告
05 C 语言与内存:指针、分配与释放
Python 的异常处理已掌握,下一篇切换到 C 语言,从指针开始理解内存地址、malloc/free 手动分配,以及内存泄漏的成因与检测。这是理解操作系统和底层安全的基础。
关注公众号获取更多内容