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

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

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

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

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

⏱️ 预计阅读时间: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 语句块可以包含 elsefinally 子句。

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 垂直技术社区,欢迎活跃、内容共建。

更多推荐