← 编程起步:做一个学习记录小助手

第 06 课:让错误输入停在计算之前

学习目标:制定数据契约,用 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,但强行转换会改变输入语义,掩盖错误。

下一步

核心已经能处理和拒绝数据。下一课给它接上真正的命令行入口,把一条用户命令贯穿到文件保存。