Python(三十三) 异常处理专题教程
目录
- 生活化类比:什么是异常?
- 基础语法:try / except / else / finally
- 核心操作:异常捕获
- 核心操作:异常抛出(raise)
- 进阶特性:自定义异常
- 进阶特性:Python 异常层级结构
- 最佳实践: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都会执行- 误区:以为
else和finally功能一样 → 正解: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是"最终防线",即使有return、break、continue也会执行- 误区:把业务逻辑写在
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),会连SystemExit、KeyboardInterrupt都捕获- 规则:越具体的异常越靠前,越宽泛的异常越靠后
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而不是raise→raise保留完整 traceback,raise e会重置 traceback- 注意:抛出的异常类型必须和
except声明的类型匹配,否则无法被捕获
五、进阶特性:自定义异常
5.1 为什么要自定义异常?
生活类比:学校有通用的"违纪条例"(ValueError、TypeError),但每个班级还可能有自己的"班规"(自定义异常)。用通用条例描述班规问题不够精准,需要专门定制。
在项目中的实际价值:
| 场景 | 不用自定义异常 | 用自定义异常 |
|---|---|---|
| 学生成绩为负数 | 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的子类,OSError是Exception的子类- 误区:捕获
BaseException作为兜底 → 正解:应用Exception,避免干扰系统退出和用户中断- 误区:认为异常只有
Exception和ValueError两种 → 正解: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("类型错误")
点击查看答案
三个错误:
except ValueError e:语法错误,应该是except ValueError as e:except Exception在except TypeError前面,TypeError是Exception的子类,所以 TypeError 永远不会被触发- 逻辑问题:
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 的返回值
解析:finally 在 return 之前执行,但不影响 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()出错。 - 代码 B:
else中的process(data)不会被except捕获。如果process(data)抛出异常,它会向上传播,不会被误吞。
结论:用 else 可以精确区分"加载数据的异常"和"处理数据的异常",让异常处理更精准。
题目 10:综合应用题
题目:编写一个完整的 safe_division_calculator() 程序,要求满足:
- 提示用户输入被除数和除数
- 如果输入的不是数字,友好提示并允许重新输入(最多 3 次机会)
- 如果除数为零,提示"除数不能为零"
- 每次计算完成后询问是否继续,输入
q退出 - 使用自定义异常
InputRetryExceededError处理超过重试次数的情况 - 使用
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 → "这个情况我处理不了,向上报告"
三条核心原则:
- 只捕获能处理的异常 —— 不知道怎么办就让它继续向上抛
- 异常信息要有上下文 —— 方便排查问题
- finally 只做资源释放 —— 文件关闭、数据库连接关闭、网络断开