第 11 章 错误处理与异常

1. 什么是异常?

在 Python 程序从编写到运行的整个生命周期里,难免会遇到各类问题,这些问题主要可归为以下两类核心类型:

(1)语法错误

语法错误是 Python 代码违反语法规则时触发的错误,这类错误会直接导致程序无法启动运行,Python 解释器会精准提示错误位置和原因。

典型示例:编写if条件语句时,末尾遗漏冒号(:)是最常见的语法错误之一。错误代码:

复制代码
age = 18
if age >= 18
    print("成年")

运行上述代码,Python 解释器会抛出SyntaxError(语法错误),提示if age >= 18这一行存在无效语法,明确指出缺少冒号,只有补充冒号修正为以下代码,程序才能正常执行:

复制代码
age = 18
if age >= 18:
    print("成年")

② 逻辑错误(程序漏洞)

逻辑错误的核心特征是:代码语法完全符合 Python 规范,程序能正常运行,但运行结果与预期不符。这类错误无法被 Python 解释器识别,需程序员通过逻辑梳理、结果验证等方式定位修正。典型示例 :计算圆的面积时,误用错误的计算公式是典型的逻辑错误。圆的面积正确公式为面积 = π × 半径²,若错误写成面积 = π × 半径 × 2(混淆了面积与周长公式),就会出现逻辑错误。错误代码:

复制代码
import math
radius = 5
# 错误:误用周长公式计算面积
circle_area = math.pi * radius * 2
print("圆的面积:", circle_area)  # 输出约31.4159,与预期的78.5398不符

修正后的正确代码:

复制代码
import math
radius = 5
# 正确:使用面积公式 π×r²
circle_area = math.pi * (radius **2)
print("圆的面积:", circle_area)  # 输出约78.5398,符合预期

(3)异常

异常是一类特殊的程序问题:代码本身既无语法错误,也不存在逻辑漏洞,但程序运行过程中因遇到意外情况而无法正常执行。

① 典型示例

  • 文件操作异常:使用open()函数读取文件时,若目标文件不存在,程序会触发FileNotFoundError异常。示例代码(触发异常):

    尝试读取不存在的文件

    f = open("nonexistent_file.txt", "r")

运行后 Python 会抛出异常,提示文件不存在,程序终止运行。

  • 输入类型异常:程序要求用户输入整数类型的年龄,但用户输入了文字内容(如 "我十岁了。"),会触发ValueError异常。示例代码(触发异常):

    要求输入整数年龄,却输入文字

    age = int(input("请输入你的年龄:")) # 输入"我十岁了。"时触发异常

② 异常处理的两种传统方法

处理异常有两种核心思路:

  • LBYL(Look Before You Leap,三思而后行):在执行操作前,先全面检查所有可能导致异常的条件,确认无风险后再执行。例如读取文件前,先检查文件是否存在;接收输入前,先校验输入内容是否为整数。
  • EAFP(Easier to Ask for Forgiveness than Permission,请求原谅比请求许可更容易):不预先检查条件,直接执行操作,若触发异常则捕获并处理异常。

③ Python 的异常处理原则

在 Python 中,优先采用 EAFP 方法处理异常,这是 Python 编程的惯用范式。

补充说明:"Leap"(跳跃)一词源自英语谚语 "A leap in the dark"(黑暗中的一跃),形象体现了 EAFP"先执行、后处理异常" 的核心思想 ------ 不提前纠结所有潜在风险,而是大胆执行操作,再对意外情况兜底。

2. EAFP 与 LBYL

在异常处理的两种传统思路中,EAFP(请求原谅比请求许可更容易)是 Python 的首选范式,但 LBYL(三思而后行)也有其适用场景。以下从优势、典型案例、适用边界三方面详细解析:

(1)EAFP 的核心优势

相较于 LBYL,EAFP 具备三大核心优势:

① 提升代码可读性

采用 LBYL 时,代码中会充斥大量前置条件检查逻辑,核心业务逻辑被错误处理代码淹没,可读性大幅降低;而 EAFP 以核心业务逻辑为主体,异常处理逻辑独立分离,代码结构更清晰。

② 执行效率更高

LBYL 要求在执行核心操作前,逐一检查所有可能触发异常的条件,这些检查会增加额外的性能开销;EAFP 无需前置检查,直接执行核心操作,仅在异常发生时处理,通常运行速度更快。

③ 减少竞态条件

竞态条件是操作系统中多线程场景的典型问题:两个或多个线程同时访问、修改同一个对象,导致程序行为异常。EAFP 能有效减少这类问题,而 LBYL 因 "检查 - 执行" 的时间间隔,极易触发竞态条件。

④ 竞态条件典型案例(LBYL 的弊端)

LBYL 思路下,读取文件的代码逻辑如下:

复制代码
import os
# LBYL:先检查文件是否存在,再读取
if os.path.exists("data.txt"):
    # 检查后、读取前,文件可能被其他程序/线程删除
    with open("data.txt", "r") as f:
        content = f.read()

上述代码中,即便检查时文件存在,也无法保证 "检查完成到打开文件" 的间隙,文件不会被其他程序删除,最终仍可能触发FileNotFoundError,导致程序崩溃。

而 EAFP 思路能规避该问题:

复制代码
# EAFP:直接执行操作,异常时捕获处理
try:
    with open("data.txt", "r") as f:
        content = f.read()
except FileNotFoundError:
    # 处理文件不存在的异常
    print("文件不存在,请确认文件路径!")

EAFP 无需前置检查,直接执行文件读取操作,即便文件被意外删除,也能通过except块捕获异常并处理,程序不会崩溃。

(2)EAFP 并非万能:LBYL 的适用场景

EAFP 是 Python 的优选方案,但并非适用于所有场景,以下情况更适合使用 LBYL:

① 处理计算量大、耗时的任务

若核心操作是计算密集型任务(如大规模数据运算、复杂算法执行),采用 EAFP 会先执行耗时操作,若触发异常则全部计算白费,整体耗时远高于前置检查;此时用 LBYL 提前校验条件(如输入数据格式、计算资源是否充足),能避免无效的耗时计算。

② 执行高风险、故障排查成本高的重要任务

若核心操作涉及关键业务(如金融交易、系统核心配置修改),一旦出错会造成严重后果,且故障追溯、定位成本极高,需通过 LBYL 前置全面检查所有条件(如权限、数据合法性、系统状态),尽可能从源头避免异常,而非依赖事后的异常处理。

3. Python 异常

(1)Python 异常的本质:异常类的实例对象

在 Python 中,所有异常本质上都是对应异常类的实例对象。简单来说,当程序触发 "文件不存在""输入类型错误" 等问题时,Python 会创建一个特定异常类的对象来表征这个错误(即便暂时不理解 "类" 和 "对象" 的概念也无需担心,后续示例会直观展示这一特性)。

① 异常类的继承特性

Python 异常类遵循面向对象编程(OOP)的核心原则 ------继承:一个异常类会从另一个异常类继承属性和行为。所有 Python 异常类最终都追溯至最顶层的 BaseException 类,不同类型的异常(如文件不存在、类型错误)则是其下层的子类。

② 完整的异常类继承关系可参考 Python 官方文档: https://docs.python.org/3/library/exceptions.html

(2)Python 异常处理的基础:try 代码块

当我们执行可能触发异常的代码时,核心处理方式是将这部分代码包裹在 try 代码块中 ------ 这是 EAFP 异常处理范式的基础操作,目的是捕获代码执行过程中可能出现的异常,避免程序直接崩溃。

① 基础语法结构

复制代码
try:
    # 放入可能引发异常的代码
    可能出错的操作
except:
    # 异常触发时执行的处理逻辑
    异常处理代码

② 直观示例

以读取文件为例,将 "打开不存在的文件" 这一风险操作放入 try 块:

复制代码
try:
    # 可能触发FileNotFoundError的代码
    f = open("nonexistent_file.txt", "r")
except FileNotFoundError:
    # 捕获并处理文件不存在的异常
    print("错误:目标文件不存在,请检查文件路径!")

上述代码中,open() 操作被包裹在 try 块中,即便文件不存在触发异常,程序也会执行 except 块中的处理逻辑,而非直接终止运行。

4. 异常处理的通用语法

Python 异常处理的完整语法包含 try、多 exceptelsefinally 四个核心模块,各部分分工明确,可覆盖不同场景下的异常与流程控制。

(1)语法结构与各模块作用

复制代码
try:
    # 核心代码:可能触发异常的操作写在此处
except 异常类1 as 变量1:
    # 异常处理1:当触发"异常类1"时,执行这里的代码
except 异常类2 as 变量2:
    # 异常处理2:当触发"异常类2"时,执行这里的代码
else:
    # 无异常执行:若try块代码无异常,执行这里的代码
finally:
    # 最终执行:无论是否出现异常,都会执行这里的代码

各模块的核心作用:

  • try 块:包裹可能触发异常的核心业务代码,是异常监控的目标区域。
  • except 块:针对不同类型的异常,分别编写对应的处理逻辑(比如文件不存在用 FileNotFoundError,类型错误用 ValueError)。
    • as 变量:可将异常对象赋值给变量,方便后续获取异常详情(如错误信息)。
  • else 块:仅当 try 块代码无异常执行完毕时,才会运行这里的代码(常用于 "无异常时的后续操作")。
  • finally 块:无论 try 块是否触发异常、except 块是否执行,finally 块的代码一定会运行(常用于资源释放,如关闭文件、断开连接)。

(2)场景示例

以 "读取文件并处理输入" 为例,展示完整语法的使用:

复制代码
try:
    # 核心操作:打开文件 + 读取内容 + 转换为整数
    with open("age_data.txt", "r") as f:
        age_str = f.read().strip()
        age = int(age_str)
except FileNotFoundError as e:
    # 处理"文件不存在"异常
    print(f"错误:文件不存在 → {e}")
except ValueError as e:
    # 处理"内容无法转整数"异常
    print(f"错误:文件内容不是有效整数 → {e}")
else:
    # 无异常时,执行后续逻辑
    print(f"读取到的年龄:{age}")
finally:
    # 无论是否异常,都会执行(此处示例打印收尾信息)
    print("文件读取操作已完成")

不同场景下的执行效果:

  • 若文件存在且内容是整数(如 "20"):执行 try → 跳过 except → 执行 else(打印 "读取到的年龄:20")→ 执行 finally(打印 "操作已完成")。
  • 若文件不存在:触发 FileNotFoundError → 执行对应 except(打印文件不存在错误)→ 跳过 else → 执行 finally
  • 若文件存在但内容是 "二十":触发 ValueError → 执行对应 except(打印内容无效错误)→ 跳过 else → 执行 finally

5. 常见错误与异常

在 Python 学习与实际开发过程中,程序报错并不是坏事。通过错误和异常信息,开发者可以快速定位问题所在。本节结合具体示例,介绍几种最常见的 Python 内置异常类型,帮助读者建立基本的错误识别能力。

(1)类型错误(TypeError)

当对不兼容的数据类型执行操作时,会触发类型错误。

复制代码
# 字符串与整数不能直接相加
result = "年龄是:" + 18

该代码中,字符串与整数类型不一致,Python 无法完成运算,因此抛出 TypeError

(2)零除错误(ZeroDivisionError)

在算术运算中,除数为 0 会触发零除错误。

复制代码
# 除数为 0
result = 10 / 0

该异常提示开发者在进行除法运算前,应注意对除数进行有效性判断。

(3)名称错误(NameError)

当程序引用了尚未定义的变量或函数名时,会产生名称错误。

复制代码
# 使用未定义的变量
print(total_score)

通常是变量拼写错误或遗漏定义语句导致。

(4)递归错误(RecursionError)

递归调用层级超过 Python 允许的最大深度时,会触发递归错误。

复制代码
def test():
    test()
test()

该函数缺少终止条件,导致无限递归调用。

(5)键错误(KeyError)

访问字典中不存在的键时,会抛出键错误。

复制代码
student = {"name": "Tom", "age": 18}
print(student["score"])

在字典操作中,应确保所访问的键是存在的。

(6)索引错误(IndexError)

当访问序列中不存在的索引位置时,会产生索引错误。

复制代码
numbers = [1, 2, 3]
print(numbers[5])

该异常通常与列表长度判断或循环边界有关。

(7)值错误(ValueError)

当参数类型正确,但取值不合法时,会触发值错误。

复制代码
# 无法将非数字字符串转换为整数
age = int("十岁")

这类错误多发生在数据转换或用户输入处理中。

(8)文件未找到错误(FileNotFoundError)

尝试以读取模式打开一个不存在的文件时,会抛出文件未找到错误。

复制代码
file = open("data.txt", "r")

该异常常见于文件路径错误或文件缺失的情况。

6. 主动抛出异常

在实际开发中,我们编写的函数往往并不是只供自己使用,而是会被其他开发者或模块调用。如果使用者以不正确的方式调用函数(例如传入非法参数),仅依赖系统自动抛出的异常往往不足以准确表达错误原因。此时,主动抛出异常是一种更加清晰、规范的做法。

在 Python 中,所有异常类型都必须是 BaseException 类的子类。内置异常(如 ValueErrorTypeError 等)已经满足这一条件,因此在多数情况下可以直接使用这些内置异常。如果需要表达更具体的业务错误,也可以自定义异常类,但自定义异常同样必须继承自 BaseException(通常继承自 Exception)。

当函数需要在特定条件下中断执行并向调用方报告错误时,可以使用 raise 关键字主动抛出异常。

(1)使用内置异常主动抛出错误

下面的示例中,函数对参数进行检查,当参数不符合要求时,主动抛出 ValueError 异常:

复制代码
def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("取款金额必须大于 0")
    if amount > balance:
        raise ValueError("余额不足")
    return balance - amount

当调用者传入非法参数时,函数会立即抛出异常,调用方可以选择捕获并处理该异常。

(2)自定义异常并主动抛出

在某些业务场景下,内置异常类型无法准确表达错误含义,此时可以自定义异常类:

复制代码
class AgeError(Exception):
    pass

然后在函数中使用 raise 主动抛出该异常:

复制代码
def register_user(age):
    if age < 0 or age > 120:
        raise AgeError("年龄不在合法范围内")
    print("注册成功")

通过自定义异常,可以让错误语义更加清晰,便于调用者区分不同类型的问题。

7. 异常的顺序

在 Python 中,异常处理代码的编写顺序至关重要。这是因为异常类之间存在继承关系,子类异常本质上也是父类异常的实例,异常捕获时会按照 except 语句出现的顺序依次匹配。

LookupError 为例,它是一个用于表示"查找失败"的异常基类,常见的两个子类包括:

  • IndexError:常见于列表、元组等序列访问越界;
  • KeyError:常见于字典中访问不存在的键。

由于 IndexErrorKeyError 都继承自 LookupError,因此它们的实例同样属于 LookupError 类型。

(1)错误的异常顺序示例

下面的代码中,LookupError 被写在了前面:

复制代码
try:
    data = [1, 2, 3]
    print(data[10])
except LookupError:
    print("捕获到查找错误")
except IndexError:
    print("捕获到索引错误")

当索引越界时,抛出的实际上是 IndexError,但由于 IndexErrorLookupError 的子类,程序会优先匹配到 LookupError,导致后面的 IndexError 分支永远不会执行。

(2)正确的异常顺序示例

在编写异常处理代码时,应当遵循**"先具体,后宽泛"**的原则,即先捕获子类异常,再捕获父类异常:

复制代码
try:
    data = [1, 2, 3]
    print(data[10])
except IndexError:
    print("捕获到索引错误")
except LookupError:
    print("捕获到查找错误")

这样可以针对不同类型的错误执行更精确的处理逻辑。

8. 卫语句与异常处理

在编写函数时,我们常常需要对多个前置条件进行校验。如果采用传统的嵌套 if 结构,代码往往会呈现出层层缩进的形式,例如:

复制代码
if thing_A_is_right:
    do_thing_A()
    if thing_B_is_right:
        do_thing_B()
        if thing_C_is_right:
            do_thing_C()

这种写法在逻辑上是正确的,但随着条件增多,代码的可读性和可维护性会迅速下降。这种"不断向右缩进"的结构,通常被称为嵌套条件判断,在实际项目中应尽量避免。

(1)使用卫语句优化结构

一种更清晰的写法是使用卫语句(Guard Clause)。卫语句的核心思想是:

在函数入口处尽早检查不合法情况,一旦条件不满足立即退出。

在 Python 中,卫语句常常与异常处理结合使用,通过 raise 主动抛出异常,使错误路径与正常逻辑路径清晰分离:

复制代码
def function():
    if not thing_A_is_right:
        raise Exception("A 条件不满足")
    do_thing_A()

    if not thing_B_is_right:
        raise Exception("B 条件不满足")
    do_thing_B()

    if not thing_C_is_right:
        raise Exception("C 条件不满足")
    do_thing_C()

这种写法具有以下优势:

  • 消除了多层嵌套,代码结构更加扁平;
  • 正常业务逻辑按顺序排列,更易阅读;
  • 错误条件集中在判断处,便于定位问题;
  • 非正常流程通过异常统一处理,更符合 Python 的编程风格。

(2)卫语句与异常的配合使用

在实际开发中,卫语句通常用于:

  • 参数合法性检查;
  • 资源状态校验(如文件是否存在、用户是否登录);
  • 不可继续执行的前置条件判断。

当条件不满足时,立即抛出异常,而不是继续向下执行或嵌套判断,这也是 Python 推荐的 EAFP(先执行、出错再处理)思想在函数结构层面的体现。

9. 上下文管理器

在学习 Python 的输入输出(I/O)相关内容时,我们已经接触过 with 语法。本节将进一步说明 with 语法背后的设计思想,以及它在资源管理中的作用。

在传统写法中,文件操作通常需要配合 try...except 结构使用,以防止文件不存在等异常情况,同时还必须显式地关闭文件。例如:

复制代码
try:
    file = open("hello.txt")
    data = file.read()
    file.close()
except FileNotFoundError:
    print("file not found...")

这种写法存在两个明显问题:

一是代码结构较为冗长,可读性不高;

二是如果在 read() 过程中发生其他异常,file.close() 可能无法被执行,从而造成资源未正确释放。

这种处理方式在 Java 等语言中较为常见,但在 Python 中并非最优解。

(1)with 语法与上下文管理器

从 Python 3 开始,引入了上下文管理器(Context Manager)机制,用于更安全、简洁地管理资源。当我们使用 with 关键字操作一个支持上下文管理的对象时,Python 会自动完成以下两件事情:

  • 进入 with 代码块时,自动申请并初始化资源;
  • 离开 with 代码块时,无论是否发生异常,都会自动释放资源。

使用上下文管理器后,上述文件读取代码可以简化为:

复制代码
try:
    with open("hello.txt") as file:
        data = file.read()
except FileNotFoundError:
    print("file not found...")

在该写法中,我们不再需要显式调用 close() 方法。当程序执行完 with 代码块后,文件会被自动关闭,从而避免资源泄漏问题。

(2)上下文管理器的应用场景

上下文管理器不仅适用于文件操作,在以下场景中同样非常常见:

  • 输入输出(I/O)操作;
  • 数据库连接与事务处理;
  • 网络连接管理;
  • 锁(Lock)等并发资源的管理。

通过 with 语法,可以将资源的获取与释放逻辑交由语言机制处理,开发者只需关注核心业务代码,从而提高程序的健壮性和可维护性。

10. Pylint 工具

Pylint 是一款常用的 Python 静态代码分析工具,可在不运行程序的情况下,对源代码进行检查。它主要用于发现潜在的编程错误、辅助执行编码规范、识别代码异味,并在一定程度上提供重构建议。

在使用前,需要先在命令行中安装该工具:

复制代码
pip install pylint

安装完成后,即可在命令行中对 Python 文件或项目运行 Pylint。

(1)动态语言中的问题发现

在 Java、C++ 等静态语言中,编译器会在编译阶段帮助开发者发现大量语法和类型错误;而 Python 属于动态语言,许多问题只有在运行时才会暴露,这对开发者的代码质量提出了更高要求。

例如,下面这段代码存在明显问题:

复制代码
def hello():
    for volume in [1, 2, 3]:
        print(volume)
    for volme in [1, 2, 3]:
        print(volume)

从表面看,代码语法正确,但第二个循环中变量名 volme 与前面的 volume 不一致,同时 print(volume) 仍然使用了旧变量。这类错误在运行前不易察觉,却可能导致逻辑错误或潜在隐患。

Pylint 正是通过静态分析和启发式算法来发现这类问题,例如未定义变量、未使用变量、变量命名不规范等,从而提前暴露代码缺陷。

(2)Pylint 的局限性

尽管 Pylint 功能强大,但在实际工程中,并非所有项目都会采用它。一些知名的 Python 项目(如 Twisted、Django、Flask、Sphinx 等)并未强制使用 Pylint。

其主要原因在于:

  • Pylint 对代码风格和结构有较为严格的要求;
  • 在大型项目中,可能会产生大量"无关紧要"的警告;
  • 过多的提示反而会干扰开发效率。

因此,Pylint 并不适合"开箱即用"地应用于所有项目。

(3)合理使用 Pylint

在实际开发中,更推荐的做法是按需使用 Pylint。我们可以在项目根目录中创建一个 .pylintrc 配置文件,根据项目特点关闭不必要的检查规则,仅保留对当前项目真正有价值的分析项。

通过合理配置,Pylint 可以成为帮助发现低级错误、提升代码质量的有效工具,而不是开发过程中的负担。

11. 单元测试

在软件开发过程中,验证代码行为的正确性是一项重要工作。Python 标准库中提供了 unittest 模块,用于编写和执行单元测试,是 Python 官方支持的测试框架之一。

单元测试的核心思想是:

将程序拆分为最小的可测试单元(通常是函数或方法),并分别验证它们在不同输入条件下的行为是否符合预期。

通过为函数编写单元测试,可以带来以下好处:

  • 确保函数在正常情况和异常情况下都能得到正确结果;
  • 降低代码修改带来的风险,便于后续重构;
  • 使函数更容易与项目的其他模块进行集成;
  • 在项目规模扩大时,提升代码的可维护性和可靠性。

unittest 模块提供了测试用例组织、断言机制以及测试运行等功能,使开发者能够系统化地对代码进行验证。在实际开发中,良好的单元测试习惯往往是高质量项目的重要基础。

假设我们有一个简单函数,用于计算两个数的和:

复制代码
def add(a, b):
    return a + b

可以为它编写单元测试如下:

复制代码
import unittest

class TestAddFunction(unittest.TestCase):
    def test_positive_numbers(self):
        self.assertEqual(add(2, 3), 5)

    def test_negative_numbers(self):
        self.assertEqual(add(-1, -1), -2)

if __name__ == "__main__":
    unittest.main()

说明:

  • 每个测试用例是 unittest.TestCase 的子类方法;
  • 使用 assertEqual 等断言方法验证函数返回值是否符合预期;
  • 运行脚本后,框架会自动执行测试,并报告成功或失败情况。
相关推荐
Lululaurel6 小时前
AI编程文本挖掘提示词实战
人工智能·python·机器学习·ai·ai编程·提示词
HappRobot7 小时前
Python 面向对象
开发语言·python
BoBoZz197 小时前
AlignTwoPolyDatas 基于ICP算法的配准和相机视角切换
python·vtk·图形渲染·图形处理
嗝o゚7 小时前
Flutter与开源鸿蒙:一场“应用定义权”的静默战争,与开发者的“范式跃迁”机会
python·flutter
一只会奔跑的小橙子7 小时前
pytest安装对应的库的方法
python
ohoy7 小时前
EasyPoi 数据脱敏
开发语言·python·excel
BoBoZz198 小时前
MarchingCubes 网格数据体素化并提取等值面
python·vtk·图形渲染·图形处理
ekprada8 小时前
DAY36 复习日
开发语言·python·机器学习
爱笑的眼睛118 小时前
强化学习组件:超越Hello World的架构级思考与实践
java·人工智能·python·ai