🛡️ Python 零基础入门系列(十三):异常处理

让程序在“暴风雨”中优雅地运行,而不是轰然倒塌

📢 系列说明:
在前一篇中,我们学会了如何通过文件操作让数据“永生”。但在真实的开发环境中,程序往往会遇到各种意外情况:文件被删除了、网络断开了、用户输入了非法字符……
本文将教你如何使用**异常处理(Exception Handling)**机制,捕获并处理这些运行时错误,让你的程序更加健壮和稳定。

📅 更新时间:2026 年 3 月 30 日

🎯 本篇你将学到:常见异常类型、try-except 语句、else 和 finally 子句、主动抛出异常、自定义异常

⏱️ 预计阅读时间:40-50 分钟 | 💻 实践时间:30-40 分钟

📌 前置知识:已完成前十二篇(特别是文件操作和逻辑判断)

✍️ 作者:书到用时方恨少!


🌟 前言:程序的“意外”时刻

在之前的练习中,如果你输入了错误的代码(比如用数字除以字符串),Python 解释器会直接报错并停止运行,终端里会打印出一长串红色的英文信息。

在编程术语中,这被称为异常(Exception)。如果不处理这些异常,程序就会非正常终止(Crash)。

异常处理的核心思想是:“防患于未然”。我们预判某些代码可能会出错,于是提前写好“应急预案”。当错误发生时,程序不会崩溃,而是执行我们准备好的处理逻辑(比如提示用户重新输入、记录日志、使用默认值等)。


1. 异常基础

1.1 什么是异常(Exception)

异常是 Python 中的一个对象,它代表程序运行过程中发生的错误。当 Python 无法正常处理代码时,就会创建一个异常对象。

1.2 常见的内置异常类型

Python 内置了许多异常类型,用来区分不同的错误场景。以下是开发中最常遇到的几种:

异常类型说明触发场景
SyntaxError语法错误代码写错了(如缺少冒号、括号不匹配),无法被捕获,必须修改代码
IndentationError缩进错误缩进不规范
NameError名称错误使用了未定义的变量名
TypeError类型错误操作或函数应用于不适当类型的对象(如 '2' + 2)
ValueError值错误对象类型正确,但值不合法(如 int('abc'))
IndexError索引错误序列下标越界
KeyError键错误字典中找不到指定的键
FileNotFoundError文件不存在试图打开不存在的文件
ZeroDivisionError除零错误除数为零

1.3 异常的传播(Traceback)

当你没有处理异常时,它会沿着函数调用栈向上传播,直到被处理或导致程序终止。

示例:

def divide(a, b):
    return a / b # 这里可能抛出 ZeroDivisionError

def main():
    divide(10, 0) # 调用 divide

main() # 调用 main

如果发生除零错误,Python 会打印:

Traceback (most recent call last):
  File "test.py", line 7, in <module>
    main()
  File "test.py", line 5, in main
    divide(10, 0)
  File "test.py", line 2, in divide
    return a / b
ZeroDivisionError: division by zero

这就是堆栈跟踪(Traceback),它能帮你精准定位错误发生的位置。


2. 捕获异常

2.1 try-except 语句

这是异常处理的核心语法。基本结构是:先尝试执行 try 块中的代码,如果出错,就跳转到 except 块处理。

语法:

try:
    # 可能出现错误的代码
    risky_code()
except:
    # 如果出错,执行这里的代码
    handle_error()

2.2 捕获特定异常

永远不要裸奔 except:。如果不指定异常类型,它会捕获所有错误,包括你意想不到的致命错误,这会让调试变得非常困难。

正确写法:

try:
    num = int(input("请输入一个数字: "))
except ValueError: # 只捕获类型转换错误
    print("输入的不是有效数字,请重新输入!")

2.3 捕获多个异常

如果一段代码可能抛出多种不同的异常,你可以使用多个 except 块。

try:
    file = open('data.txt', 'r')
    content = file.read()
    number = int(content)
    result = 10 / number
except FileNotFoundError:
    print("错误:找不到文件 data.txt")
except ValueError:
    print("错误:文件内容不是有效的数字")
except ZeroDivisionError:
    print("错误:不能除以零")

2.4 捕获异常对象(as)

有时候你不仅想知道出错了,还想获取具体的错误信息(比如错误原因)。可以使用 as 关键字。

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"程序出错了:{e}") # 输出:程序出错了:division by zero

2.5 万能捕获(慎用)

虽然可以使用 Exception 来捕获几乎所有的常规错误,但通常建议将其放在 except 块的最后,作为兜底方案。

try:
    # 一些复杂的操作
    pass
except FileNotFoundError as e:
    print(f"文件错误: {e}")
except Exception as e: # 捕获其他所有未预料到的错误
    print(f"未知错误: {e}")
    # 通常在这里记录日志,方便后续排查

3. 完整的异常处理结构

一个完整的 try 语句块可以包含 else 和 finally 子句。

3.1 else 子句

else 块在 try 块没有发生任何异常时执行。它通常用于放置那些只有在没有错误时才应该运行的代码,以避免意外捕获这些代码产生的错误。

对比:

# 方式 A:没有 else
try:
    num = int(input("输入数字: "))
    print(f"你输入了: {num}")
    # 如果 print 报错,也会被下面的 except 捕获(虽然概率极低)
except ValueError:
    print("输入错误")
# 方式 B:推荐使用 else
try:
    num = int(input("输入数字: "))
except ValueError:
    print("输入错误")
else:
    # 只有转换成功才会执行
    print(f"你输入了: {num}")
    # 这里的 print 报错会直接抛出,不会被上面的 except 捕获

3.2 finally 子句

finally 块无论是否发生异常,都会在最后执行。它通常用于释放关键资源,例如关闭文件、关闭数据库连接、释放锁等。

语法:

try:
    file = open("test.txt", "w")
    file.write("Hello")
    # 假设这里发生了一个未被捕获的致命错误
    raise RuntimeError("模拟崩溃")
except ValueError:
    print("处理值错误")
finally:
    # 即使上面 raise 了 RuntimeError,这里依然会执行!
    file.close()
    print("文件已关闭")

执行顺序总结:

  1. 执行 try 块。
  2. 如果有异常,寻找匹配的 except 块并执行。
  3. 如果没有异常,执行 else 块。
  4. 无论前面发生了什么(即使程序要崩溃了),最后都必须执行 finally 块。

4. 主动抛出异常

除了 Python 自动抛出的异常,我们也可以在代码中主动抛出异常。

4.1 raise 语句

当你检测到某种条件不满足,无法继续执行时,可以使用 raise 抛出异常。

示例:

def withdraw_money(balance, amount):
    if amount > balance:
        # 主动抛出异常,终止函数执行
        raise ValueError("余额不足,无法取款!")
    balance -= amount
    return balance

# 调用
try:
    withdraw_money(100, 200)
except ValueError as e:
    print(e) # 输出:余额不足,无法取款!

4.2 重新抛出异常

在 except 块中处理完日志记录等逻辑后,如果你想让上层调用者也知道这个错误,可以使用 raise 不带参数重新抛出当前的异常。

try:
    risky_operation()
except Exception as e:
    print(f"记录日志:操作失败,原因:{e}")
    # 记录完日志后,继续向上抛出错误,让上层处理
    raise 

5. 自定义异常

Python 内置的异常类型虽然很多,但有时无法精确表达业务逻辑中的错误。这时我们可以创建自定义异常。

方法:定义一个继承自 Exception 类(或其子类)的新类。

场景:开发一个在线购物系统,需要处理“库存不足”的业务异常。

# 自定义异常类
class InsufficientStockError(Exception):
    """库存不足异常"""
    def __init__(self, product_name, stock, requested):
        self.product_name = product_name
        self.stock = stock
        self.requested = requested
        # 调用父类构造函数
        super().__init__(f"商品 '{product_name}' 库存不足。库存: {stock}, 请求: {requested}")

# 模拟下单函数
def place_order(product, quantity):
    stock = 10 # 假设库存只有 10 个
    if quantity > stock:
        # 抛出自定义异常
        raise InsufficientStockError(product, stock, quantity)
    print(f"订单成功:购买 {quantity} 个 {product}")

# 调用与处理
try:
    place_order("iPhone", 15)
except InsufficientStockError as e:
    print(f"业务错误:{e}")
    print(f"请联系客服。商品:{e.product_name}")

6. 异常处理最佳实践

  1. 具体优于宽泛:
    尽量捕获具体的异常类型(如 ValueError),而不是裸奔 except:。这能防止掩盖真正的 Bug。
  2. 不要忽略异常:
    即使你写了 except,也至少要打印日志或记录下来。空的 except 块会让程序在出错时静默失败,极难排查。
  3. 使用 finally 释放资源:
    文件、网络连接、锁等资源必须在 finally 中释放,或者使用 with 语句(上下文管理器)自动管理。
  4. 尽早抛出,晚点捕获:
    在错误发生的源头尽早抛出异常,但在有足够信息处理它的地方才捕获。
  5. 利用异常传递信息:
    通过自定义异常,可以将业务逻辑中的错误清晰地表达出来。

7. 综合实战:带异常处理的文件备份工具

我们将结合前几篇学的文件操作、os 模块和异常处理,编写一个健壮的文件备份脚本。

功能要求:

  1. 用户输入源文件路径和目标备份路径。
  2. 程序检查源文件是否存在。
  3. 程序尝试复制文件。
  4. 处理各种可能的错误(路径无效、权限不足、磁盘满等)。
import os
import shutil

def backup_file():
    print("=== 欢迎使用安全文件备份工具 ===")
    
    src = input("请输入要备份的文件路径: ").strip()
    dst = input("请输入备份目标路径(含文件名): ").strip()

    try:
        # 1. 验证源文件
        if not os.path.exists(src):
            raise FileNotFoundError(f"源文件不存在:{src}")
        
        if not os.path.isfile(src):
            raise ValueError(f"路径存在,但不是一个文件:{src}")

        # 2. 验证目标目录
        dst_dir = os.path.dirname(dst)
        if dst_dir and not os.path.exists(dst_dir):
            # 询问是否创建目录
            create = input(f"目标目录不存在 {dst_dir},是否自动创建?(y/N): ")
            if create.lower() != 'y':
                return # 用户取消
            os.makedirs(dst_dir, exist_ok=True)

        # 3. 执行备份
        print(f"正在备份 {src} 到 {dst} ...")
        shutil.copy2(src, dst) # copy2 会保留元数据
        print("✅ 备份成功!")

    except PermissionError as e:
        print(f"❌ 权限错误:无法读取源文件或写入目标位置。")
        print(f"   详情:{e}")
    
    except FileNotFoundError as e:
        print(f"❌ 文件未找到错误:{e}")
    
    except OSError as e:
        # 捕获磁盘空间不足等操作系统级错误
        print(f"❌ 磁盘或系统错误:可能是磁盘已满或路径过长。")
        print(f"   详情:{e}")
    
    except Exception as e:
        # 兜底异常
        print(f"❌ 发生未知错误:{e}")
    
    finally:
        print("操作结束。")

if __name__ == "__main__":
    backup_file()

🎉 结语

通过本篇的学习,你已经掌握了 Python 程序的“安全带”——异常处理。

  • 你学会了使用 try-except-else-finally 结构来捕获和处理错误。
  • 你理解了如何通过 raise 主动抛出异常。
  • 你掌握了自定义异常来处理业务逻辑错误。
  • 你学会了编写健壮的代码,防止程序因意外输入而崩溃。

现在,你的程序已经具备了应对“意外”的能力。

下一步建议:
回顾你之前写的代码(比如计算器或文件操作脚本),尝试加入异常处理机制,看看程序的稳定性提升了多少。

🚀 下一篇预告:《Python 零基础入门系列(十四):综合实战项目》
这是本系列的收官之作。我们将把前 13 篇学到的所有知识(语法、函数、模块、文件、异常等)融合在一起,从零开始开发一个完整的 “个人记账管理系统”。敬请期待!

📅 更新时间:2026 年 3 月 30 日
✍️ 作者:书到用时方恨少!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐