Python 异常处理

真实程序一定会遇到意外:用户输入的不是数字、文件不存在、网络断了、API 超时。异常处理让程序"出了错也不崩溃",而是优雅地处理。这一章把 Python 异常机制讲透——从 try/except 到自定义异常、上下文管理器。

1. 为什么需要异常处理?

没有异常处理,程序一出错就直接崩溃退出,用户体验极差。异常处理让你捕获错误、记录日志、给用户友好提示、继续运行。它是健壮程序的基础。

2. try / except / else / finally

# 基础结构:try / except / else / finally
try:
    n = int(input("请输入一个数字:"))
    result = 10 / n
except ValueError:
    # 捕获特定异常
    print("那不是有效的数字!")
except ZeroDivisionError:
    print("不能除以零!")
except Exception as e:
    # 兜底:捕获所有其他异常
    print(f"发生了未知错误:{e}")
else:
    # 没异常时才执行(常用于"成功后的逻辑")
    print(f"结果是 {result}")
finally:
    # 无论是否有异常都会执行(常用于清理资源)
    print("计算结束")

# 示例运行:
# 输入 5  -> 结果是 2.0 / 计算结束
# 输入 abc -> 那不是有效的数字! / 计算结束
# 输入 0  -> 不能除以零! / 计算结束

四个块的作用:

3. 常见内置异常

# Python 内置异常类(层次结构,部分)
# BaseException
# ├── SystemExit              sys.exit() 触发
# ├── KeyboardInterrupt       Ctrl+C 中断
# └── Exception               所有"普通"异常的基类
#     ├── ValueError          值错误(如 int("abc"))
#     ├── TypeError           类型错误(如 "a" + 1)
#     ├── KeyError            字典 key 不存在
#     ├── IndexError          索引越界
#     ├── AttributeError      属性不存在
#     ├── FileNotFoundError   文件不存在
#     ├── ZeroDivisionError   除以零
#     ├── NameError           变量未定义
#     ├── StopIteration       迭代结束
#     ├── OverflowError       数值溢出
#     └── ... 还有很多

# 例子:各种常见异常
try:
    x = [1, 2, 3]
    print(x[10])               # IndexError
except IndexError:
    print("索引越界")

try:
    d = {"a": 1}
    print(d["b"])              # KeyError
except KeyError as e:
    print(f"key 不存在:{e}")   # key 不存在:'b'

try:
    f = open("不存在.txt")      # FileNotFoundError
except FileNotFoundError:
    print("文件不存在")

记住这些常见异常能帮你快速读懂错误信息。所有异常都继承自 Exception(顶层是 BaseException,但日常只用 Exception 子类)。

4. raise:主动抛出异常

# raise:主动抛出异常
def divide(a, b):
    if b == 0:
        # 主动抛出,提前拦截错误
        raise ValueError("除数不能为零")
    return a / b

# 调用方需要处理这个异常
try:
    result = divide(10, 0)
except ValueError as e:
    print(f"出错:{e}")      # 出错:除数不能为零

# raise 不带参数:重新抛出当前异常(在 except 块里)
try:
    n = int("abc")
except ValueError:
    print("记录日志...")
    raise                    # 把异常继续往外抛,让上层处理

主动抛异常用于"防御式编程":在函数入口检查参数,不合法就抛异常,避免错误传播到深处。

5. 自定义异常

真实项目里,内置异常不够用,需要自定义业务异常。规则:继承 Exception(不要继承 BaseException)。

# 自定义异常:继承 Exception 类
class WithdrawError(Exception):
    """取款异常的基类"""
    pass

class InsufficientBalanceError(WithdrawError):
    """余额不足"""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(
            f"余额不足:当前 {balance},要取 {amount}"
        )

class NegativeAmountError(WithdrawError):
    """取款金额必须为正数"""
    pass


class Account:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount <= 0:
            raise NegativeAmountError("取款金额必须大于 0")
        if amount > self.balance:
            raise InsufficientBalanceError(self.balance, amount)
        self.balance -= amount
        return self.balance


# 使用
acc = Account(100)
try:
    acc.withdraw(150)
except InsufficientBalanceError as e:
    print(f"取款失败:{e}")
    print(f"你的余额:{e.balance},尝试取 {e.amount}")
# 输出:
# 取款失败:余额不足:当前 100,要取 150
# 你的余额:100,尝试取 150

自定义异常的好处:异常类型即业务语义——InsufficientBalanceErrorValueError 更能说明问题。可以携带额外信息(如余额、金额),供调用方处理。

6. 异常链:raise ... from

# 异常链:raise ... from 表示"由哪个异常引起"
def parse_and_double(s):
    try:
        n = int(s)
        return n * 2
    except ValueError as e:
        # 把底层异常包装成更有意义的异常
        raise ValueError(f"无法解析 '{s}' 为数字") from e

try:
    parse_and_double("abc")
except ValueError as e:
    print(f"主异常:{e}")
    print(f"引起原因:{e.__cause__}")
# 输出:
# 主异常:无法解析 'abc' 为数字
# 引起原因:invalid literal for int() with base 10: 'abc'

当你把底层异常"包装"成更高级的异常时,用 from 保留原因,方便调试。e.__cause__ 能拿到原始异常。

7. with 语句与上下文管理器

前面讲文件操作时,我们大量用了 with open(...)。它的本质是上下文管理器协议:任何实现 __enter____exit__ 的类都能用 with。

# with 语句的本质:上下文管理器
# 任何实现了 __enter__ 和 __exit__ 的类都能用 with

class MyResource:
    def __enter__(self):
        print("获取资源")
        return self              # with ... as x 中的 x 就是返回值

    def __exit__(self, exc_type, exc_val, exc_tb):
        # exc_type/val/tb:如果有异常,这里是异常信息;没有就是 None
        print("释放资源")
        # 返回 True 表示"吞掉异常",返回 False/None 让异常继续传播
        return False

    def do_work(self):
        print("使用资源")


# 用法
with MyResource() as r:
    r.do_work()
# 输出:
# 获取资源
# 使用资源
# 释放资源

# open() 返回的文件对象就是一个上下文管理器
# 所以 with open(...) 能自动关闭文件

自己写资源类(数据库连接、锁、网络会话)时,实现这两个方法就能用 with,自动管理资源生命周期。

8. 最佳实践(重要!)

# 异常处理的最佳实践

# 1. ⚠ 不要用裸 except(吞掉所有错误,隐藏 bug)
try:
    do_something()
except:                    # ❌ 反例
    pass                   # 错误被悄悄吞掉

# ✅ 应该捕获具体异常
try:
    do_something()
except FileNotFoundError:
    handle_not_found()
except ValueError as e:
    handle_value_error(e)

# 2. 不要捕获后什么都不做(pass)
try:
    risky()
except SomeError:
    pass                   # ❌ 至少记日志或重新抛出

# 3. 异常信息要具体,不要丢失 traceback
import logging
try:
    risky()
except SomeError as e:
    logging.exception("操作失败")    # 记录完整堆栈
    raise                              # 重新抛出

# 4. 不要用异常处理做流程控制(性能差,代码乱)
# ❌ 反例:用 try/except 判断 key 是否存在
# try:
#     value = d["key"]
# except KeyError:
#     value = "default"
# ✅ 用 get
value = d.get("key", "default")

核心原则:

小结

异常处理让程序健壮。重点掌握:try/except/else/finally常见异常类型raise自定义异常(继承 Exception)、异常链 fromwith 上下文管理器最佳实践(捕获具体、不吞异常、不做流程控制)。下一篇看日期时间——Python 时间处理。

← 上一篇 Python 文件操作

下一篇 Python 日期时间

✈️💬