📌 引言:人生总有意外,程序也是

想象一下:你正在厨房精心准备晚餐,切菜时发现——刀不见了!你打开冰箱拿鸡蛋,结果盒子是空的!你打开燃气灶,发现煤气没开!

如果做饭没有预案,这些小意外就会让你手忙脚乱。同样,程序运行时也会遇到各种“意外”:

  • 文件明明存在,打开时却被删了
  • 用户输入了字符串,你却期待数字
  • 网络请求突然超时

如果没有应对措施,程序就会直接崩溃,留下一堆看不懂的报错信息。异常处理就是给程序准备的“应急预案”,让它遇到意外时能优雅地处理,而不是直接躺平。

本章我们就来学习:

  • 什么是异常
  • 如何捕获和处理异常
  • 如何主动抛出异常
  • 如何定义自己的异常
  • 断言这个小工具怎么用

一、异常是什么?—— 程序的“感冒发烧”

异常是程序运行过程中发生的错误或意外情况。当异常发生时,Python 会中断正常执行流程,并抛出异常对象。如果没有处理,程序就会终止并显示错误信息(traceback)。

# 一个经典的崩溃
print(10 / 0)  # ZeroDivisionError: division by zero

常见的内置异常

异常 触发场景
ZeroDivisionError 除以零
FileNotFoundError 文件不存在
ValueError 值类型正确但内容不合法(如 int(“abc”))
TypeError 操作或函数应用于不适当类型
IndexError 列表索引超出范围
KeyError 字典键不存在
AttributeError 访问不存在的属性
NameError 使用未定义的变量

异常和错误的区别:错误通常指语法错误(程序根本跑不起来),异常是运行时发生的问题。但日常口语中常混用。


二、try-except:捕获异常,拯救程序

1️⃣ 基本语法

try:
    # 可能出错的代码
    result = 10 / 0
except ZeroDivisionError:
    # 处理特定异常
    print("除数不能为0!")

如果 try 块中发生了 ZeroDivisionError,程序会立即跳转到对应的 except 块执行,然后继续执行后面的代码(不会崩溃)。

2️⃣ 捕获多个异常

可以用元组同时捕获多种异常:

try:
    num = int(input("请输入一个数字:"))
    result = 100 / num
except (ValueError, ZeroDivisionError):
    print("输入必须是非零数字!")

也可以用多个 except 块分别处理:

try:
    num = int(input("请输入一个数字:"))
    result = 100 / num
except ValueError:
    print("这不是一个有效的数字!")
except ZeroDivisionError:
    print("数字不能为零!")

3️⃣ 获取异常实例

如果想得到异常的具体信息,可以用 as e

try:
    with open('nofile.txt', 'r') as f:
        content = f.read()
except FileNotFoundError as e:
    print(f"文件未找到:{e}")  # e 包含了错误详情

4️⃣ 捕获所有异常(谨慎!)

try:
    # 可能出错的代码
except Exception as e:
    print(f"出错了:{e}")

Exception 是所有常规异常的基类,这样能捕获大部分异常,但不推荐滥用,因为可能隐藏你没预料到的 bug,比如 KeyboardInterrupt(Ctrl+C)不会被捕获(它继承自 BaseException 而非 Exception)。更糟糕的是,except: 不带异常类型会捕获所有异常,包括 SystemExitKeyboardInterrupt,让你连程序都关不掉。

最佳实践:只捕获你预料到并知道如何处理的异常。


三、else 和 finally:收尾工作

1️⃣ else:没有异常时执行

try:
    num = int(input("请输入一个数字:"))
except ValueError:
    print("输入不是数字")
else:
    print(f"你输入了 {num},真棒!")  # 只有没发生异常时执行

else 块在 try 没有抛出任何异常时执行,可以避免把“正常代码”也放进 try 块,减少误捕获的可能性。

2️⃣ finally:无论是否异常都执行

f = None
try:
    f = open('test.txt', 'r')
    content = f.read()
except FileNotFoundError:
    print("文件不存在")
finally:
    if f:
        f.close()  # 无论如何都要关闭文件
    print("清理工作完成")

finally 块中的代码一定会执行,即使 tryexcept 中有 returnbreakcontinue。常用于释放资源(关闭文件、数据库连接等)。

现代写法:资源管理大多用 with 语句,它自动处理了类似 finally 的清理工作,所以 finally 现在用得少了,但在某些场景(如手动加锁/解锁)仍然有用。


🧠 冷知识:try-except-else-finally 的执行顺序

  • 如果 try 正常 → 执行 else → 最后 finally
  • 如果 try 异常 → 执行匹配的 except → 最后 finally
  • 如果 tryexcept 中有 returnfinally 依然会在返回前执行
  • 如果 finally 中有 return,它会覆盖之前的 return
def test():
    try:
        return "try"
    finally:
        return "finally"  # 覆盖了 try 的 return

print(test())  # 输出 finally

别这么写,可读性极差。


四、raise:主动抛出异常

有时候程序逻辑自己发现了问题,你想主动制造一个异常,告诉调用者“出事了”。这时用 raise

1️⃣ 抛出内置异常

def withdraw(balance, amount):
    if amount > balance:
        raise ValueError("余额不足")
    return balance - amount

try:
    new_balance = withdraw(100, 200)
except ValueError as e:
    print(e)  # 余额不足

2️⃣ 重新抛出异常

捕获异常后,可能部分处理,然后希望继续向上层抛出:

try:
    process_data()
except ValueError:
    log_error()
    raise  # 重新抛出当前异常,保留调用栈

单纯的 raise 不加参数会重新抛出当前异常,常用于在记录日志后继续传播。

3️⃣ 抛出自定义异常(见下一节)


五、自定义异常:打造你自己的“警报器”

当内置异常不足以描述你的业务错误时,可以创建自己的异常类。只需继承 Exception(或其子类)。

class BalanceError(Exception):
    """余额不足的自定义异常"""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"余额 {balance} 不足,需要 {amount}")

def withdraw(balance, amount):
    if amount > balance:
        raise BalanceError(balance, amount)
    return balance - amount

try:
    withdraw(100, 200)
except BalanceError as e:
    print(e)  # 余额 100 不足,需要 200
    print(f"当前余额:{e.balance}")  # 可以访问自定义属性

自定义异常让错误信息更丰富,也方便调用者针对性地捕获。


六、断言:快速调试的利器

断言用于检查“应该永远为真”的条件,如果条件为假,程序会抛出 AssertionError

def divide(a, b):
    assert b != 0, "除数不能为零"
    return a / b

divide(10, 0)  # AssertionError: 除数不能为零

使用场景

  • 调试时检查参数合法性
  • 验证程序内部状态
  • 作为文档,表明某些假设

注意事项

  • 断言可以用 -O 命令行选项全局禁用(python -O script.py),所以不要用断言做数据验证或安全检查,验证用户输入应该用 if 并抛异常。
  • 断言失败意味着程序有 bug,而不是运行时错误。
# ❌ 错误用法
def process(user_id):
    assert user_id is not None, "user_id 不能为空"
    # 如果禁用断言,user_id 为 None 时不会报错,导致后续崩溃

# ✅ 正确用法
def process(user_id):
    if user_id is None:
        raise ValueError("user_id 不能为空")

七、异常处理的“反模式”

❌ 空 except 或 except: 捕获所有异常

try:
    # something
except:
    pass  # 什么都抓,连 Ctrl+C 都抓,程序关不掉!

❌ 捕获异常但不处理(pass)

try:
    risky_operation()
except SomeError:
    pass  # 错误被吞了,调试时会很痛苦

至少应该记录日志。

❌ 用异常控制正常流程

# 糟糕的代码
try:
    value = my_dict[key]
except KeyError:
    value = default

应该用 if key in my_dictget() 方法。异常用于意外情况,不是常规逻辑。

❌ 在 except 块里引发新异常时不保留原始信息

try:
    convert()
except ValueError:
    raise MyError("转换失败")  # 丢失了原始异常信息

可以用 raise MyError("转换失败") from e 来保留原因。


八、异常处理的最佳实践

  1. 只捕获你打算处理的异常,不要过度捕获。
  2. 尽量具体:捕获 FileNotFoundError 而不是 Exception
  3. 在合适的地方处理:底层函数让异常向上传播,顶层再捕获。
  4. 使用 finally 释放资源,但能用 with 就用 with
  5. 自定义异常时继承 Exception,保持清晰。
  6. 用断言检查内部一致性,但不要依赖它做数据验证。
  7. 记录异常日志,便于排查问题。

✅ 本章总结

概念 说明
异常 运行时发生的错误
try-except 捕获并处理异常
多个 except 分别处理不同类型的异常
else 无异常时执行
finally 无论是否异常都执行
raise 主动抛出异常
自定义异常 继承 Exception,增加业务语义
assert 调试用断言,生产可禁用
反模式 空 except、吞异常、滥用异常

记住:异常处理不是让程序不崩溃,而是让程序在崩溃时能优雅地告诉我们为什么,并有机会恢复或安全退出。


🚀 下集预告

掌握了异常处理,你的程序已经具备了基本的健壮性。下一章我们将进入 Python 最核心也最迷人的部分——面向对象编程。类、对象、继承、多态……这些概念将彻底改变你组织代码的方式。准备好拥抱 OOP 了吗?我们即将开启新世界的大门!

现在,去写点带异常处理的代码吧:试试写一个健壮的计算器,能处理各种非法输入;或者写个文件读取函数,遇到错误能给出友好提示。实践出真知!🛡️


🔗 上一篇:文件操作:与硬盘对话的艺术
🔗 下一篇:面向对象编程:把代码当成积木搭(待更新)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐