课程进度 课程大纲 已发布 24/24 课
Python 基础
数据与集合
构建可靠的程序
使用对象建模
专业 Python
高级 Python
异常就像警报
第 9 章为每个函数规定了契约。如果某个输入违背了这个约定,会发生什么?Python 会停止当前执行路径,并引发一个异常。这是有用的信息,并不是什么怪物。
- 停止 某个操作无法完成
- 阅读 从最后一行开始
- 确定名称 类型加消息
- 选择 修复、拒绝或恢复
下面这行代码会故意失败:
minutes = int("forty")
回溯信息的末尾是一条 ValueError 消息。先阅读最后一行,然后向上查找你文件中的对应代码行。不同的异常类型指向不同的契约违背情况:int(None) 会引发 TypeError,而 10 / 0 会引发 ZeroDivisionError。
只捕获你理解的警报
try 代码块应该非常短。里面只放你打算处理的那个有风险的操作。
raw_minutes = "forty"
try:
minutes = int(raw_minutes)
except ValueError:
print("Minutes must be a whole number.")
else:
print("Accepted:", minutes)
输出:
Minutes must be a whole number.
except ValueError 的范围很精确:它只处理自己理解的转换失败。不要使用不指定类型的 except:,也不要仅仅为了让程序继续运行而捕获 Exception。它们可能会掩盖拼写错误和计算错误。
把后续的格式化操作放在 else 中。这样,格式化代码里的缺陷就不会被误认为是输入错误。只有当程序边界确实会以相同方式处理多种异常时,才同时捕获多个类型,例如使用 except (TypeError, ValueError) 处理文本或 None 输入。
四条路径各有各的职责
- try 尝试一个有风险的步骤
- except 处理一个预期中的失败
- else 使用成功得到的结果
- finally 始终执行清理工作
def show_average(total, count):
try:
average = total / count
except ZeroDivisionError:
print("No sessions to average.")
else:
print(f"Average: {average:.1f}")
finally:
print("Calculation finished.")
show_average(90, 3)
show_average(0, 0)
输出:
Average: 30.0
Calculation finished.
No sessions to average.
Calculation finished.
else 只会在操作成功后运行。无论操作成功、异常被捕获,还是出现未被捕获的异常,finally 都会运行。请用它执行真正的清理工作,不要用它隐藏错误或替换返回值。第 12 章会介绍一种用于清理文件资源的更简单工具。
验证是一道关卡,不是一次猜测
转换一个值和检查它的含义是两道不同的关卡。"0" 可以转换成整数 0,但规则可能仍然不允许使用零。
- 原始输入 "45" 或 "many"
- 转换 它能变成整数吗?
- 验证 允许使用 1 到 1440 之间的数吗?
def parse_minutes(raw_minutes):
try:
minutes = int(raw_minutes)
except (TypeError, ValueError) as cause:
raise ValueError("minutes must be a whole number") from cause
if not 1 <= minutes <= 1440:
raise ValueError("minutes must be from 1 to 1440")
return minutes
raise ... from cause 可以向调用方提供一条稳定且有帮助的消息,同时将最初的转换失败保留在 error.__cause__ 和回溯信息中。
当对象的类型不符合接口要求时,使用 TypeError;当对象类型可以接受、但它的值不合法时,使用 ValueError。对于必需却无效的数据,不要悄悄用零代替。还有一个容易忽略的边缘情况:int(True) 会得到 1;如果你的契约规定布尔值不能表示分钟数,就要明确拒绝布尔值。
构建一道可信的学习关卡
将这个完整项目保存为 study_gate.py:
def normalize_topic(raw_topic):
if not isinstance(raw_topic, str):
raise TypeError("topic must be text")
topic = raw_topic.strip()
if not topic:
raise ValueError("topic must not be empty")
return topic
def parse_minutes(raw_minutes):
if isinstance(raw_minutes, bool):
raise TypeError("minutes must not be a Boolean")
try:
minutes = int(raw_minutes)
except (TypeError, ValueError) as cause:
raise ValueError("minutes must be a whole number") from cause
if not 1 <= minutes <= 1440:
raise ValueError("minutes must be from 1 to 1440")
return minutes
def make_session(raw_topic, raw_minutes):
return {
"topic": normalize_topic(raw_topic),
"minutes": parse_minutes(raw_minutes),
}
def process_entries(raw_entries):
accepted = []
problems = []
for number, (topic, minutes) in enumerate(raw_entries, start=1):
try:
session = make_session(topic, minutes)
except (TypeError, ValueError) as error:
problems.append(f"Entry {number}: {error}")
else:
accepted.append(session)
return accepted, problems
def format_report(sessions, problems):
total = 0
for session in sessions:
total += session["minutes"]
lines = [
f"Accepted: {len(sessions)}",
f"Rejected: {len(problems)}",
f"Trusted minutes: {total}",
]
for problem in problems:
lines.append("- " + problem)
return "\n".join(lines)
def run_checks():
assert normalize_topic(" Python ") == "Python"
assert parse_minutes("45") == 45
try:
parse_minutes("many")
except ValueError as error:
assert str(error) == "minutes must be a whole number"
assert isinstance(error.__cause__, ValueError)
else:
raise AssertionError("bad minutes were accepted")
run_checks()
raw_entries = [
(" Python ", "45"),
("Git", "many"),
(" ", "15"),
("SQL", "30"),
("Testing", "0"),
]
sessions, problems = process_entries(raw_entries)
print("Checks passed.")
print(format_report(sessions, problems))
运行 python3 study_gate.py:
Checks passed.
Accepted: 2
Rejected: 3
Trusted minutes: 75
- Entry 2: minutes must be a whole number
- Entry 3: topic must not be empty
- Entry 5: minutes must be from 1 to 1440
集合边界只会捕获它预期的验证失败。程序缺陷仍会清楚地暴露出来,而不会被说成“无效输入”。
三个小任务
- 编写
parse_rating(),让它接受 1 到 5 之间的整数,并测试 0、1、5 和 6。 - 从一个字典中删除
minutes键;只捕获KeyError,并把成功时的打印操作放在else中。 - 在项目数据中加入
(42, "10")和("Git", True),并预测这两条消息。
准备好学习第 11 章了吗?
- 我会从最后一行开始阅读回溯信息。
- 我会保持
try简短,并且只捕获预期的异常类型。 - 我能解释
except、else和finally。 - 我会先进行转换,再验证含义。
- 我会使用
raise ... from ...保留异常原因。 - 我运行了学习关卡,并且所有检查都已通过。
接下来,你会把这些可信的辅助函数移入模块和包中,并为每个项目提供自己的环境。