目 录CONTENT

文章目录

Python(三十三) 异常处理专题教程

Python(三十三) 异常处理专题教程

目录

  1. 生活化类比:什么是异常?
  2. 基础语法:try / except / else / finally
  3. 核心操作:异常捕获
  4. 核心操作:异常抛出(raise)
  5. 进阶特性:自定义异常
  6. 进阶特性:Python 异常层级结构
  7. 最佳实践:8 条黄金法则
  8. 随堂练习题(10 道)

一、生活化类比:什么是异常?

1.1 用"课堂点名"理解异常

想象一下:老师在课堂上点名让学生回答问题。

  • 正常流程:老师点名 → 学生站起来 → 回答问题 → 坐下(一切顺利)
  • 异常情况
    • 学生请假了(人不在)→ 老师跳过,点下一个
    • 学生答错了 → 老师纠正,给出正确答案
    • 教室突然停电 → 启用应急灯,安排自习

在 Python 中,异常(Exception)就是程序运行中出现的"意外情况"

# 正常流程:除法计算
result = 10 / 2    # 一切正常,result = 5.0

# 异常情况:除以零
result = 10 / 0    # ZeroDivisionError!程序崩溃

如果不处理异常,程序会直接崩溃退出。就像老师遇到停电不处理,课堂就乱套了。而异常处理机制就是一套"应急预案",让程序遇到问题时不崩溃,而是优雅地应对。

1.2 异常处理的核心思想

课堂场景 Python 异常处理
点名(正常流程) try 块中的代码
学生请假(已知意外) except 捕获特定异常
无人请假(一切顺利) else 无异常时执行
下课收尾(无论如何都要做) finally 总是执行

二、基础语法:try / except / else / finally

2.1 四个关键字的官方定义

关键字 官方定义 触发时机
try 包裹可能出错的代码,让 Python 监视这段代码的执行 最先执行,从头到尾
except try 中发生指定类型的异常时,执行此块 仅在匹配的异常发生时执行
else try没有任何异常时执行 仅在 try 完全成功时执行
finally 无论如何都会执行的代码块(即使前面有 return 总是最后执行,兜底保障

2.2 示例一:成绩录入(最基础用法)

# 场景:老师录入学生成绩,学生可能输入非数字内容

try:
    # try 块:放可能出错的代码
    score = int(input("请输入成绩:"))
except ValueError:
    # except 块:当输入的不是数字时触发
    print("错误:请输入一个有效的整数!")
else:
    # else 块:只有输入成功(没出错)时才执行
    print(f"成绩录入成功:{score} 分")
finally:
    # finally 块:无论成功与否都执行
    print("本次录入操作结束。")

# 运行效果分析:
# 输入 85   → "成绩录入成功:85 分" + "本次录入操作结束。"
# 输入 abc  → "错误:请输入一个有效的整数!" + "本次录入操作结束。"
# 注意:输入 abc 时,else 块不会执行!

初学者常见坑点:

  • 误区:以为 finally 只在出错时执行 → 正解:无论是否出错,finally 都会执行
  • 误区:以为 elsefinally 功能一样 → 正解else 只在无异常时执行,finally 始终执行
  • 误区:在 try 里写一大段无关代码 → 正解try 只包裹真正可能出错的代码

2.3 示例二:除法计算(展示 finally 兜底特性)

# 场景:计算两个数的商,用户可能输入 0 作为除数

def divide_numbers():
    try:
        a = float(input("被除数:"))
        b = float(input("除数:"))
        result = a / b
    except ZeroDivisionError:
        print("数学错误:除数不能为零!")
        return  # 注意:即使这里有 return,finally 仍会执行!
    except ValueError:
        print("输入错误:请输入有效的数字!")
        return
    else:
        print(f"计算结果:{a} ÷ {b} = {result}")
        return
    finally:
        # finally 的兜底特性:即使上面 return 了,这里也会执行
        print("计算器使用完毕,清理资源...")

divide_numbers()

# 运行效果分析:
# 输入 10, 2  → "计算结果:10.0 ÷ 2.0 = 5.0" → "计算器使用完毕,清理资源..."
# 输入 10, 0  → "数学错误:除数不能为零!" → "计算器使用完毕,清理资源..."
# 输入 10, abc → "输入错误:请输入有效的数字!" → "计算器使用完毕,清理资源..."
# 关键:即使 except 里有 return,finally 依然会执行!

初学者常见坑点:

  • 误区:认为 return 之后 finally 不会执行 → 正解finally 是"最终防线",即使有 returnbreakcontinue 也会执行
  • 误区:把业务逻辑写在 finally 里 → 正解finally 只用于资源释放,不写业务逻辑

2.4 示例三:文件读取(展示完整的 try/except/else/finally 协作)

# 场景:读取一个文件,文件可能存在也可能不存在

def read_student_list(filename):
    file = None  # 先初始化为 None
    try:
        # try:尝试打开并读取文件
        file = open(filename, "r", encoding="utf-8")
        content = file.read()
    except FileNotFoundError:
        # except:文件不存在
        print(f"错误:文件 '{filename}' 不存在,请先创建文件!")
    except PermissionError:
        # except:没有读取权限
        print(f"错误:没有权限读取文件 '{filename}'!")
    else:
        # else:只有读取成功才处理内容
        print("文件读取成功!内容如下:")
        print(content)
        return len(content)  # 返回文件字符数
    finally:
        # finally:无论如何都要关闭文件(释放资源)
        if file is not None:
            file.close()
            print("文件已关闭。")
        print("读取操作结束。")

read_student_list("students.txt")

# 执行顺序总结:
# 1. try 块开始执行
# 2a. 如果出错 → 跳到匹配的 except → 跳到 finally
# 2b. 如果没出错 → 执行 else → 跳到 finally
# 3. finally 始终最后执行

三、核心操作:异常捕获

3.1 多异常捕获的两种写法

写法一:分开捕获(推荐用于不同处理逻辑)

try:
    num = int(input("请输入一个数字:"))
    result = 100 / num
    data = [1, 2, 3]
    print(data[num])
except ValueError:
    # 专门处理"输入不是数字"
    print("输入不合法:请输入整数!")
except ZeroDivisionError:
    # 专门处理"除数为零"
    print("数学错误:不能除以零!")
except IndexError:
    # 专门处理"索引越界"
    print("索引越界:列表中不存在该位置!")

写法二:元组合并捕获(用于相同处理逻辑)

try:
    value = int(input("请输入数字:"))
    result = 100 / value
except (ValueError, ZeroDivisionError):
    # 两种异常统一处理
    print("输入不合法或除数为零,请重新输入!")

初学者常见坑点:

  • 误区except (ValueError, ZeroDivisionError) 写成 except ValueError, ZeroDivisionError(Python 3 语法错误)
  • 正解:必须用括号括起来,形成一个元组
  • 误区:多个 except 把子类异常写在父类异常后面

3.2 异常捕获的顺序优先级(重要!)

核心规则:子类异常必须写在父类异常前面!

Python 从上到下匹配 except,一旦匹配到就不再往下找。如果父类在前,子类永远不会被执行。

错误示例(反面教材):

try:
    num = int(input("输入数字:"))
    result = 100 / num
except Exception:       # 父类先捕获,太宽泛!
    print("出错了")
except ValueError:       # 这行永远不会执行!因为 ValueError 是 Exception 的子类
    print("输入不是数字")
except ZeroDivisionError:  # 这行也永远不会执行!
    print("不能除以零")

# 问题:所有异常都被第一个 except 拦截,后面的特定处理形同虚设

正确示例:

try:
    num = int(input("输入数字:"))
    result = 100 / num
except ValueError:          # 子类在前
    print("输入不是数字")
except ZeroDivisionError:   # 子类在前
    print("不能除以零")
except Exception:            # 父类在后,作为兜底
    print("发生了其他未知错误")

初学者常见坑点:

  • 禁止:直接写 except:except Exception: 作为第一个捕获(吞掉所有异常)
  • 禁止:写 except: 不加任何异常类型(裸 except),会连 SystemExitKeyboardInterrupt 都捕获
  • 规则:越具体的异常越靠前,越宽泛的异常越靠后

3.3 获取异常对象 as

try:
    result = 10 / 0
except ZeroDivisionError as e:  # as e 获取异常对象
    print(f"出错啦!异常类型:{type(e).__name__}")
    print(f"异常信息:{e}")
    # 输出:
    # 出错啦!异常类型:ZeroDivisionError
    # 异常信息:division by zero

初学者常见坑点:

  • 误区:写成 except ZeroDivisionError e(少了 as,语法错误)
  • 正解:必须是 except 异常类型 as 变量名
  • 注意e 变量的作用域只在 except 块内

四、核心操作:异常抛出(raise)

4.1 为什么要主动抛出异常?

当你的代码检测到"不符合预期"的状态时,应该主动抛出异常,而不是默默忽略。

类比:你是班长,发现有人在教室吃榴莲(不符合规定),你应该主动报告老师(抛出异常),而不是假装没看见。

4.2 三种 raise 语法

语法一:抛出原生异常

def set_age(age):
    if age < 0:
        raise ValueError  # 直接抛出 ValueError,无自定义信息
    print(f"年龄设置为:{age}")

# set_age(-5)  # ValueError(信息不明确)

语法二:抛出带自定义信息的异常(推荐)

def set_age(age):
    if age < 0:
        raise ValueError(f"年龄不能为负数!你输入的是:{age}")
    if age > 150:
        raise ValueError(f"年龄不能超过150岁!你输入的是:{age}")
    print(f"年龄设置成功:{age}")

# set_age(-5)  →  ValueError: 年龄不能为负数!你输入的是:-5
# set_age(200) →  ValueError: 年龄不能超过150岁!你输入的是:200
# set_age(18)  →  年龄设置成功:18

语法三:重新抛出捕获到的异常(异常链)

def read_config(filename):
    try:
        with open(filename, "r", encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError as e:
        # 记录日志后重新抛出,让上层调用者也知道
        print(f"[日志] 配置文件 {filename} 未找到")
        raise  # 重新抛出原始异常,保持异常类型不变

# read_config("config.json")
# 输出:
# [日志] 配置文件 config.json 未找到
# FileNotFoundError: [Errno 2] No such file or directory: 'config.json'

初学者常见坑点:

  • 误区raise 后面加括号,写成 raise ValueError() → 虽不报错,但 raise ValueError("msg") 更符合习惯
  • 误区:重新抛出时写了 raise e 而不是 raiseraise 保留完整 traceback,raise e 会重置 traceback
  • 注意:抛出的异常类型必须和 except 声明的类型匹配,否则无法被捕获

五、进阶特性:自定义异常

5.1 为什么要自定义异常?

生活类比:学校有通用的"违纪条例"(ValueErrorTypeError),但每个班级还可能有自己的"班规"(自定义异常)。用通用条例描述班规问题不够精准,需要专门定制。

在项目中的实际价值

场景 不用自定义异常 用自定义异常
学生成绩为负数 ValueError("负数") InvalidScoreError("成绩不能为负数")
学生不存在 ValueError("学生") StudentNotFoundError("张三不存在")
成绩超出范围 ValueError("范围") ScoreOutOfRangeError("成绩必须在0-100之间")

自定义异常让错误类型一目了然,方便调用方按需捕获。

5.2 定义规则

# 规则:自定义异常必须继承自 Exception,不能继承 BaseException
# 原因:BaseException 包含 SystemExit、KeyboardInterrupt 等系统级异常,不应干扰

class InvalidScoreError(Exception):
    """成绩不合法异常"""
    pass  # 简单异常,不需要额外逻辑

class StudentNotFoundError(Exception):
    """学生不存在异常"""
    def __init__(self, student_name):
        self.student_name = student_name
        super().__init__(f"学生 '{student_name}' 不存在")

class ScoreOutOfRangeError(Exception):
    """成绩超范围异常"""
    def __init__(self, score, min_score=0, max_score=100):
        self.score = score
        self.min_score = min_score
        self.max_score = max_score
        super().__init__(f"成绩 {score} 不在 [{min_score}, {max_score}] 范围内")

5.3 完整案例:学生成绩管理系统

# ===== 第一步:定义自定义异常体系 =====

class ScoreSystemError(Exception):
    """成绩系统基础异常(所有业务异常的父类)"""
    pass

class InvalidScoreError(ScoreSystemError):
    """成绩不合法(负数或非数字等)"""
    def __init__(self, score, reason=""):
        self.score = score
        self.reason = reason
        super().__init__(f"无效成绩 {score}:{reason}")

class ScoreOutOfRangeError(ScoreSystemError):
    """成绩超出 0-100 范围"""
    def __init__(self, score):
        self.score = score
        super().__init__(f"成绩 {score} 超出有效范围 [0, 100]")

class StudentNotFoundError(ScoreSystemError):
    """学生不存在"""
    def __init__(self, student_name):
        self.student_name = student_name
        super().__init__(f"学生 '{student_name}' 不在系统中")

class DatabaseConnectionError(ScoreSystemError):
    """数据库连接失败(模拟)"""
    def __init__(self, message="数据库连接失败"):
        super().__init__(message)


# ===== 第二步:模拟学生数据库 =====

students_db = {
    "张三": {"math": 85, "english": 90},
    "李四": {"math": 72, "english": 68},
    "王五": {"math": 95, "english": 88},
}


# ===== 第三步:编写业务函数(层层抛出异常) =====

def validate_score(score):
    """校验单个成绩的合法性"""
    if not isinstance(score, (int, float)):
        raise InvalidScoreError(score, "成绩必须是数字")
    if score < 0:
        raise InvalidScoreError(score, "成绩不能为负数")
    if score > 100:
        raise ScoreOutOfRangeError(score)
    return True


def get_student_score(student_name, subject):
    """从数据库中获取学生成绩(模拟)"""
    # 模拟数据库连接(可能失败)
    import random
    if random.random() < 0.1:  # 10% 概率模拟数据库故障
        raise DatabaseConnectionError()

    if student_name not in students_db:
        raise StudentNotFoundError(student_name)

    if subject not in students_db[student_name]:
        raise InvalidScoreError(subject, f"科目 '{subject}' 不存在")

    score = students_db[student_name][subject]
    validate_score(score)  # 校验成绩合法性
    return score


def print_student_report(student_name, subject):
    """打印学生成绩单(最上层调用,统一处理所有异常)"""
    try:
        score = get_student_score(student_name, subject)
    except StudentNotFoundError as e:
        print(f"【学生不存在】{e}")
        print("  请检查学生姓名是否正确。")
    except InvalidScoreError as e:
        print(f"【成绩数据异常】{e}")
        print("  请联系管理员修复数据。")
    except ScoreOutOfRangeError as e:
        print(f"【成绩范围异常】{e}")
    except DatabaseConnectionError as e:
        print(f"【系统故障】{e}")
        print("  请稍后重试。")
    except ScoreSystemError as e:
        # 兜底:捕获所有业务异常
        print(f"【未知系统错误】{e}")
    else:
        print(f"学生 {student_name} 的 {subject} 成绩为:{score} 分")
    finally:
        print("-" * 40)


# ===== 第四步:测试 =====

print_student_report("张三", "math")    # 正常:85 分
print_student_report("赵六", "math")    # 异常:学生不存在
print_student_report("张三", "physics") # 异常:科目不存在

运行效果分析:

学生 张三 的 math 成绩为:85 分
----------------------------------------
【学生不存在】学生 '赵六' 不在系统中
  请检查学生姓名是否正确。
----------------------------------------
【成绩数据异常】无效成绩 physics:科目 'physics' 不存在
  请联系管理员修复数据。
----------------------------------------

5.4 自定义异常的项目落地价值

项目结构中的异常体系:

ScoreSystemError(基础)
├── InvalidScoreError(数据校验)
├── ScoreOutOfRangeError(范围校验)
├── StudentNotFoundError(业务查询)
└── DatabaseConnectionError(基础设施)

好处:
1. 调用方可以按粒度捕获:只关心数据库异常?捕获 DatabaseConnectionError 即可
2. 也可以整体捕获:捕获 ScoreSystemError 处理所有业务异常
3. 错误信息自带业务语义,日志清晰
4. 新人接手代码时,一看异常类就知道哪里出了什么问题

初学者常见坑点:

  • 误区:继承 BaseException正解:必须继承 Exception
  • 误区:自定义异常类写了很多复杂逻辑 → 正解:异常类保持简单,主要承载错误描述
  • 误区:每个小问题都新建一个异常类 → 正解:够用就好,按业务模块分类

六、进阶特性:Python 异常层级结构

6.1 异常继承层级图

BaseException(所有异常的祖宗)
├── SystemExit              # sys.exit() 触发
├── KeyboardInterrupt       # Ctrl+C 触发
├── GeneratorExit           # 生成器关闭
└── Exception               # 所有"普通异常"的父类 ← 自定义异常继承这里
    ├── ArithmeticError     # 算术错误
    │   ├── ZeroDivisionError   # 除以零
    │   ├── OverflowError       # 数值溢出
    │   └── FloatingPointError  # 浮点运算错误
    ├── ValueError          # 值错误(类型对但值不对)
    ├── TypeError           # 类型错误
    ├── IndexError          # 索引越界
    ├── KeyError            # 字典键不存在
    ├── AttributeError      # 属性不存在
    ├── NameError           # 变量名未定义
    ├── FileNotFoundError   # 文件不存在(OSError 的子类)
    ├── OSError             # 操作系统错误
    │   ├── FileNotFoundError
    │   ├── PermissionError
    │   └── IsADirectoryError
    ├── ImportError         # 导入模块失败
    │   └── ModuleNotFoundError
    ├── StopIteration       # 迭代器结束
    └── SyntaxError         # 语法错误(通常代码运行前就报错)

6.2 理解层级关系的重要性

错误示例(大异常包小异常):

try:
    data = {"name": "小明"}
    print(data["age"])  # KeyError
except Exception:       # 太宽泛!把 KeyError、ValueError 都吞了
    print("出错了")      # 无法知道到底是什么错

# 问题:如果文件不存在、网络断了、数据库挂了,全显示"出错了"
# 调试时完全不知道问题在哪

正确示例(按层级精确捕获):

try:
    data = {"name": "小明"}
    print(data["age"])
except KeyError as e:
    # 精确知道是"键不存在"
    print(f"字典中缺少键:{e}")
except Exception as e:
    # 兜底,但记录了详细信息
    print(f"未知错误:{type(e).__name__} - {e}")

6.3 不同层级异常的捕获范围对比

def test_capture_range():
    try:
        # 模拟不同错误(取消注释来测试)
        # 1 / 0           # 触发 ZeroDivisionError
        # {}["key"]       # 触发 KeyError
        # [][0]           # 触发 IndexError
        value = int("abc")  # 触发 ValueError
    except ZeroDivisionError:
        print("→ 只捕获除以零")
    except ArithmeticError:
        print("→ 捕获所有算术错误(包括 ZeroDivisionError)")
    except LookupError:
        print("→ 捕获所有查找错误(包括 IndexError 和 KeyError)")
    except ValueError:
        print("→ 只捕获值错误")
    except Exception:
        print("→ 兜底:捕获所有普通异常")
    # 注意:这里 ValueError 在当前行会触发,输出 "→ 只捕获值错误"

test_capture_range()

关键结论:

异常类 捕获范围
ZeroDivisionError 仅除以零
ArithmeticError 所有算术错误(含子类)
LookupError 所有查找错误(含 IndexError、KeyError)
OSError 所有系统错误(含 FileNotFoundError、PermissionError)
Exception 所有普通异常
BaseException 所有异常(含 SystemExit 等,通常不应捕获)

初学者常见坑点:

  • 误区FileNotFoundError 不是 Exception 的子类 → 正解:它是 OSError 的子类,OSErrorException 的子类
  • 误区:捕获 BaseException 作为兜底 → 正解:应用 Exception,避免干扰系统退出和用户中断
  • 误区:认为异常只有 ExceptionValueError 两种 → 正解:Python 异常体系很丰富,按需使用

七、最佳实践:8 条黄金法则

法则 1:只捕获能处理的异常,不吞掉未知异常

错误做法:

try:
    result = complex_calculation()
except Exception:
    pass  # 把异常吞了!不知道发生了什么
# 后续代码可能用了一个错误的 result,导致更难排查的 bug

正确做法:

try:
    result = complex_calculation()
except ValueError as e:
    print(f"数值格式错误:{e}")
    result = 0  # 已知可处理的异常,提供默认值
# 其他异常(如 TypeError、KeyError)会正常抛出,便于调试

法则 2:finally 仅用于资源释放

错误做法:

try:
    process_data()
finally:
    print("处理完成!")                    # 不是资源释放!
    send_notification()                    # 不是资源释放!
    update_database()                      # 不是资源释放!

正确做法:

file = None
connection = None
try:
    file = open("data.txt", "r")
    connection = connect_db()
    process_data(file, connection)
except IOError as e:
    print(f"文件或数据库错误:{e}")
finally:
    # 只做资源释放!
    if file:
        file.close()
    if connection:
        connection.close()

法则 3:异常信息要包含足够的调试上下文

错误做法:

try:
    user = find_user(user_id)
except Exception:
    raise RuntimeError("查询失败")  # 不知道哪个用户、什么原因

正确做法:

try:
    user = find_user(user_id)
except Exception as e:
    raise RuntimeError(
        f"查询用户失败 | user_id={user_id} | "
        f"原始错误: {type(e).__name__}: {e}"
    ) from e  # from e 保留异常链

法则 4:避免在 try 块中包裹过多代码

错误做法:

try:
    user_input = input("输入:")
    data = parse(user_input)
    result = calculate(data)
    save_to_db(result)
    send_email(result)
    print("完成")
except Exception:
    print("出错了")  # 完全不知道哪一步出了问题!

正确做法:

try:
    user_input = input("输入:")
    data = parse(user_input)
except ValueError as e:
    print(f"输入解析错误:{e}")
    return

try:
    result = calculate(data)
except ArithmeticError as e:
    print(f"计算错误:{e}")
    return

save_to_db(result)      # 这步出错应单独处理或直接向上抛出
send_email(result)      # 这步出错应单独处理或直接向上抛出
print("完成")

法则 5:避免在循环内滥用 try-except

错误做法:

# 处理 100 万条数据,每条都套 try-except,性能极差
for item in huge_list:
    try:
        process(item)
    except Exception:
        continue

正确做法:

# 方案一:先过滤掉明显会出错的数据
valid_items = [item for item in huge_list if is_valid(item)]
for item in valid_items:
    process(item)

# 方案二:如果必须捕获,把 try 提到循环外面
total_errors = 0
for item in huge_list:
    total_errors += process_safe(item)  # 函数内部不做 try-except

# 方案三:仅捕获预期会发生的特定异常
for item in huge_list:
    try:
        process(item)
    except ValueError:  # 只捕获你预料到会出现的错误
        log_error(item)

法则 6:使用 raise from 保留异常链

错误做法:

try:
    config = parse_file("config.json")
except FileNotFoundError:
    raise ConfigError("配置加载失败")  # 原始错误信息丢失!

正确做法:

try:
    config = parse_file("config.json")
except FileNotFoundError as e:
    raise ConfigError("配置加载失败") from e
    # from e 保留了原始的 FileNotFoundError,完整 traceback 都在

法则 7:永远不要用裸 except 或 except Exception 开头

错误做法:

try:
    dangerous_operation()
except:  # 裸 except!会捕获 KeyboardInterrupt、SystemExit 等
    pass

正确做法:

try:
    dangerous_operation()
except ValueError as e:
    handle_value_error(e)
except IOError as e:
    handle_io_error(e)
except Exception as e:  # 即便兜底也要加 as e 记录信息
    log_error(e)
    raise  # 无法处理就重新抛出

法则 8:记录日志而不是只 print

错误做法:

try:
    critical_operation()
except Exception as e:
    print(f"错误:{e}")  # 只在控制台显示,程序一关就没了

正确做法:

import logging

logging.basicConfig(
    level=logging.ERROR,
    format="%(asctime)s [%(levelname)s] %(message)s",
    filename="app.log"
)

try:
    critical_operation()
except Exception as e:
    logging.error(f"关键操作失败:{e}", exc_info=True)
    # exc_info=True 会记录完整的 traceback 到日志文件

八、随堂练习题(10 道)

题目 1:基础执行顺序

题目:请写出以下代码的输出结果。

def test_order():
    try:
        print("1. try 开始")
        result = 10 / 2
        print("2. try 结束")
    except ZeroDivisionError:
        print("3. except")
    else:
        print("4. else")
    finally:
        print("5. finally")

test_order()
点击查看答案

输出:

1. try 开始
2. try 结束
4. else
5. finally

解析:没有异常,所以 except 不执行。顺序:try → else → finally。


题目 2:异常触发时的执行顺序

题目:请写出以下代码的输出结果。

def test_order2():
    try:
        print("A")
        result = 10 / 0  # 这里出错了!
        print("B")       # 这行会执行吗?
    except ZeroDivisionError:
        print("C")
    else:
        print("D")       # 这行会执行吗?
    finally:
        print("E")

test_order2()
点击查看答案

输出:

A
C
E

解析print("B") 不会执行(出错后立即跳到 except);print("D") 不会执行(else 只在无异常时执行);print("E") 始终执行。


题目 3:修正错误的异常捕获代码

题目:下面的代码有语法错误和逻辑错误,请找出并修正。

try:
    age = int(input("年龄:"))
    if age < 0:
        raise ValueError
    print("明年你", age + 1, "岁")
except ValueError e:           # 错误 1
    print("年龄无效")
except Exception:              # 错误 2
    print("其他错误")
except TypeError:              # 错误 3
    print("类型错误")
点击查看答案

三个错误:

  1. except ValueError e: 语法错误,应该是 except ValueError as e:
  2. except Exceptionexcept TypeError 前面,TypeErrorException 的子类,所以 TypeError 永远不会被触发
  3. 逻辑问题:raise ValueError 没有携带错误信息,不利于调试

修正后:

try:
    age = int(input("年龄:"))
    if age < 0:
        raise ValueError(f"年龄不能为负数:{age}")
    print("明年你", age + 1, "岁")
except ValueError as e:
    print(f"年龄无效:{e}")
except TypeError:
    print("类型错误")
except Exception:
    print("其他错误")

题目 4:finally 与 return 的执行顺序

题目:以下代码输出什么?请解释原因。

def mystery():
    try:
        return "try 的返回值"
    finally:
        print("finally 执行了!")

result = mystery()
print("result =", result)
点击查看答案

输出:

finally 执行了!
result = try 的返回值

解析finallyreturn 之前执行,但不影响 return 的值。流程是:计算 return 的值 → 执行 finally → 返回已计算的值。


题目 5:编写自定义异常实现年龄校验

题目:编写一个 age_validator(age) 函数,要求:

  • 定义自定义异常 InvalidAgeError(继承 Exception
  • 年龄不为整数时抛出 InvalidAgeError("年龄必须是整数")
  • 年龄为负数时抛出 InvalidAgeError("年龄不能为负数")
  • 年龄超过 150 时抛出 InvalidAgeError("年龄不能超过150岁")
  • 正常年龄输出 "年龄 {age} 校验通过"
点击查看答案
class InvalidAgeError(Exception):
    """年龄不合法异常"""
    pass

def age_validator(age):
    if not isinstance(age, int):
        raise InvalidAgeError("年龄必须是整数")
    if age < 0:
        raise InvalidAgeError("年龄不能为负数")
    if age > 150:
        raise InvalidAgeError("年龄不能超过150岁")
    print(f"年龄 {age} 校验通过")

# 测试
try:
    age_validator(25)    # 通过
    age_validator(-5)    # 异常
except InvalidAgeError as e:
    print(f"校验失败:{e}")

题目 6:多异常捕获顺序

题目:以下代码中,如果 data = {}(空字典),执行 data["key"] 会触发什么异常?它会进入哪个 except 分支?

data = {}

try:
    result = 100 / len(data)     # len({}) = 0,所以 100/0
    print(data["key"])
except IndexError:
    print("A: 索引越界")
except ZeroDivisionError:
    print("B: 除以零")
except KeyError:
    print("C: 键不存在")
点击查看答案

输出: B: 除以零

解析:代码按顺序执行。100 / len(data) 先执行,len({}) = 0,触发 ZeroDivisionError,直接跳到第二个 except。后面的 print(data["key"]) 根本不会执行到。


题目 7:异常链的理解

题目:以下代码的输出是什么?

def func_a():
    raise ValueError("A 出错了")

def func_b():
    try:
        func_a()
    except ValueError as e:
        raise RuntimeError("B 也出错了") from e

try:
    func_b()
except RuntimeError as e:
    print(f"捕获到:{e}")
    print(f"原因:{e.__cause__}")
点击查看答案

输出:

捕获到:B 也出错了
原因:A 出错了

解析raise ... from e 将原始异常 e 设置为新异常的 __cause__,形成异常链。这样既保留了上层对业务异常的捕获能力,又不丢失底层异常的根因。


题目 8:except 块中的错误处理

题目:以下代码有什么潜在问题?

try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    file.close()  # 问题在哪里?
点击查看答案

问题file 变量只在 try 块中赋值。如果 open() 就失败了(触发 FileNotFoundError),file 变量根本不存在,file.close() 会触发 NameError

修正:

file = None
try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("文件不存在")
finally:
    if file is not None:  # 安全检查
        file.close()

更好的写法(使用 with):

try:
    with open("data.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("文件不存在")
# with 自动关闭文件,无需手动处理

题目 9:else 的实用性

题目:以下两段代码功能相同吗?如果不同,有什么区别?

代码 A:

try:
    data = load_data()
    process(data)
except DataError:
    handle_error()

代码 B:

try:
    data = load_data()
except DataError:
    handle_error()
else:
    process(data)
点击查看答案

功能不同!

  • 代码 A:如果 process(data) 抛出 DataError,也会被 except 捕获。你可能误以为 load_data() 出错了,实际上是 process() 出错。
  • 代码 Belse 中的 process(data) 不会被 except 捕获。如果 process(data) 抛出异常,它会向上传播,不会被误吞。

结论:用 else 可以精确区分"加载数据的异常"和"处理数据的异常",让异常处理更精准。


题目 10:综合应用题

题目:编写一个完整的 safe_division_calculator() 程序,要求满足:

  1. 提示用户输入被除数和除数
  2. 如果输入的不是数字,友好提示并允许重新输入(最多 3 次机会)
  3. 如果除数为零,提示"除数不能为零"
  4. 每次计算完成后询问是否继续,输入 q 退出
  5. 使用自定义异常 InputRetryExceededError 处理超过重试次数的情况
  6. 使用 finally 在程序退出时打印"计算器已关闭"
点击查看答案
class InputRetryExceededError(Exception):
    """输入重试次数超限异常"""
    def __init__(self, max_retries):
        super().__init__(f"已超过最大重试次数 {max_retries},程序退出。")

def safe_division_calculator():
    max_retries = 3

    try:
        while True:
            # 输入被除数(带重试)
            a = None
            for attempt in range(1, max_retries + 1):
                try:
                    a = float(input("请输入被除数:"))
                    break
                except ValueError:
                    print(f"输入不合法!剩余重试次数:{max_retries - attempt}")
            else:
                # for 循环正常结束(没有 break)表示重试耗尽
                raise InputRetryExceededError(max_retries)

            # 输入除数(带重试)
            b = None
            for attempt in range(1, max_retries + 1):
                try:
                    b = float(input("请输入除数:"))
                    if b == 0:
                        raise ZeroDivisionError("除数不能为零!")
                    break
                except ValueError:
                    print(f"输入不合法!剩余重试次数:{max_retries - attempt}")
                except ZeroDivisionError as e:
                    print(e)
                    break  # 除数为零不消耗重试次数,直接跳出
            else:
                raise InputRetryExceededError(max_retries)

            if b is not None and b != 0:
                result = a / b
                print(f"计算结果:{a} ÷ {b} = {result:.2f}")

            # 询问是否继续
            choice = input("继续计算?(按 q 退出,其他键继续):")
            if choice.lower() == "q":
                print("感谢使用!")
                break

    except InputRetryExceededError as e:
        print(e)
    finally:
        print("计算器已关闭。")

# 运行
safe_division_calculator()

总结

异常处理是 Python 编程的核心技能之一。记住这张"速查表":

try      → "我来试试这个操作"
except   → "出问题了,我知道怎么处理"
else     → "一切顺利,执行后续操作"
finally  → "无论如何,这些收尾工作必须做"
raise    → "这个情况我处理不了,向上报告"

三条核心原则:

  1. 只捕获能处理的异常 —— 不知道怎么办就让它继续向上抛
  2. 异常信息要有上下文 —— 方便排查问题
  3. finally 只做资源释放 —— 文件关闭、数据库连接关闭、网络断开
0
博主关闭了当前页面的评论