学习目标:制定数据契约,用 raise 报告错误,用 try/except 在入口解释错误。
前置:第 05 课的 JSON 读取。预计 60 分钟。
本课交付:合法与非法输入表,以及能够定位到第几条记录的报错。
JSON 能解析,不代表记录合理。{"subject": "", "minutes": -5} 符合 JSON 语法,却不是本工具接受的学习记录。
本课程明确约定:主题必须是去掉两端空格后非空的文本;分钟数必须是 1–240 的整数。允许同主题多条记录;一天可以记录多次。240 是本练习设定的单条上限,不是普遍适用的学习规则。
def make_record(subject, minutes):
if not isinstance(subject, str) or not subject.strip():
raise ValueError("主题不能为空,且必须是文本")
if type(minutes) is not int or not 1 <= minutes <= 240:
raise ValueError("分钟必须是 1–240 的整数")
return {"subject": subject.strip(), "minutes": minutes}
第一个 if 先检查类型。or 从左到右判断,左侧已经为 True 就不再求右侧,因此不会对 None 调用 strip。strip() 去掉字符串两端空白,不会删除主题中间的空格。
这里刻意用 type(minutes) is int。Python 的布尔类型与整数有继承关系,isinstance(True, int) 为 True;我们的约定不把 True 当 1 分钟,因此要拒绝它。
“25”这个字符串也被拒绝。后续命令行层可以把合法数字文本先转换为整数,再交给核心校验;校验函数自身的契约保持清楚。
运行 py -3 versions/06_validation.py。它依次尝试 25、-5、"25" 和 True。
预期第一项输出主题已去掉空格的记录;另外三项分别显示“拒绝 -5”“拒绝 '25'”“拒绝 True”,原因都是分钟必须为 1–240 的整数。
调用方的关键结构:
try:
record = make_record(" Python ", minutes)
print("接受:", record)
except ValueError as error:
print(f"拒绝 {minutes!r}:{error}")
raise 使当前正常流程中断,把异常交给调用者;匹配的 except 接住后执行错误处理。出错时不会继续运行本次 try 中的“接受”那一行。
!r 使用更明确的值表示形式,所以字符串带引号,便于区分 25 与 "25"。
打开 study_core.py 的 validate_records。它先检查最外层是不是列表,再通过 enumerate(records, 1) 同时取得位置编号和记录。
对于第 2 条错误,它会补充“第 2 条记录”的上下文,再交回调用方。职责分工是:make_record 判断一条,validate_records 知道第几条,命令行入口知道如何向用户显示。
函数返回的是新整理的列表,不在检查过程中修改原输入。若中途失败,调用者不会拿到一个真假混杂的半份结果。
| 动作 | 例子 | 是否表示数据一定可用 |
|---|---|---|
| 语法解析 | json.loads(text) | 否,只说明是合法 JSON |
| 类型转换 | int("25") | 否,-5 也可以转成整数 |
| 业务校验 | 检查 1–240 | 满足本工具该字段的约定 |
| 错误报告 | “第 2 条分钟不合法” | 告诉使用者改哪里 |
不要写 except Exception: pass 把所有问题吞掉。预计发生的输入错误可以翻译成提示,程序逻辑错误应保留足够信息让开发者定位。
教材 data/invalid.json 含 -5 分钟。运行:
py -3 tracker.py --file data/invalid.json summary
07 课会解释这条命令的完整结构。现在只观察:它应报告“第 1 条记录”,不应输出成功统计,也不应修改 invalid.json。
写下 8 个输入的预测:主题空串、全空格、正常中文;分钟为 1、240、0、241、True。再调用函数逐个验证。
验收:边界 1 和 240 被接受;0 和 241 被拒绝;正常中文保留;空主题拒绝。解释为什么“转换成 int 后再接受 True”不符合本课约定。
True 是逻辑值,不是用户提供的分钟数;虽然 int(True) 为 1,但强行转换会改变输入语义,掩盖错误。
核心已经能处理和拒绝数据。下一课给它接上真正的命令行入口,把一条用户命令贯穿到文件保存。