第 8 章 · 个人里程碑 M2
菜单、状态与M2 验收
第 7 章按固定轮次处理报名和签到;真实使用者会按自己的顺序查询、报名、取消、再签到。本章用持续运行的菜单接起已有规则。重点不是多写几个分支,而是让所有操作共享同一份状态,并在连续操作后仍说得清“谁报名、谁签到、还剩几个名额”。
- 预计阅读
- 70 分钟
- 上机实践
- 90 分钟
- 前置知识
- 第 1—7 章
- 本章产出
- 个人 ClubHub M2 版
学完本章,你应该能够
- 把查询、报名、取消、签到、统计与退出接入一个可靠的菜单循环。
- 让数据只初始化一次,区分只读操作、成功更新与拒绝后不变。
- 维护名额上限和“签到者必已报名”两项状态不变量,解释取消后的连锁变化。
- 写出操作序列的逐步状态表,并用十组黑盒测试验证个人 M2 版本。
- 完成独立现场修改、同伴互测与个人复盘。
8.1 固定轮次不能处理真实操作顺序
上一章总是“先三次报名、再两次签到、最后汇总”。但使用者可能先查询活动,再报名、签到、取消,之后换另一个人报名。菜单让使用者每轮选择一种操作,程序执行后回到菜单;只有选择 0 才退出。这里仍是单个免费活动和四位虚构成员,不做真实学校系统对接。
| 输入 | 效果 | 是否另读学号 |
|---|---|---|
| 1 查询 | 输出活动字段与剩余名额,不改状态 | 否 |
| 2 报名 | 合法且未满额时加入报名集合 | 是 |
| 3 取消 | 成功时同时撤销报名与签到 | 是 |
| 4 签到 | 已报名且未签到时加入签到集合 | 是 |
| 5 统计 | 输出人数与具体学号,不改状态 | 否 |
| 0 退出 | 结束循环,输出最终汇总 | 否 |
其他输入只报告“菜单无效”,不再读学号。菜单输入先 strip(),保持字符串;"0" 是退出口令,不需要转成整数,q 不是菜单项。
8.2 状态只在循环外初始化一次
变量的作用位置直接影响程序行为。若把两份集合写在 while True 内,每次回到菜单都会重置为空,上一轮成功报名就“消失”。正确做法是循环外初始化,循环内按选项读取或更新。
registrations = set()
checkins = set()
while True:
choice = input("0退出,5统计:").strip()
if choice == "0":
break
elif choice == "5":
print(f"报名人数:{len(registrations)}")
else:
print("菜单无效")
print("程序结束")这个骨架故意只有退出和统计,用来先验证“会反复出现、能退出、非法选项不崩溃”。之后才逐个接入业务分支。break 只在选 0 时跳出菜单循环,不能把查询或错误输入也当作退出。
8.3 每个菜单入口都要写清输入、守卫、变化
菜单不是把第 7 章代码简单贴六遍。每个分支都要明确它读取什么、失败时怎样、成功后改变哪些状态。使用同一活动字典、成员字典、报名集合和签到集合,才能让统计与查询看见最新结果。
| 操作 | 按顺序检查 | 成功后状态 |
|---|---|---|
| 报名 | 未知 → 重复 → 满额 | 报名集合加入该学号 |
| 取消 | 未知 → 尚未报名 | 报名、签到集合都去掉该学号 |
| 签到 | 未知 → 尚未报名 → 已签到 | 签到集合加入该学号 |
| 查询、统计 | 无需学号;只读 | 两集合原样不变 |
例如报名集合已满时,再输入一个已经报名的学号,应报“请勿重复报名”,而不是“名额已满”;判断顺序决定了提示是否准确。空白学号清洗后不在成员字典中,按未知学号拒绝。所有被拒操作都不得悄悄改变集合。
8.4 状态不变量:取消必须同步撤销签到
“不变量”是每一次操作前后都必须成立的约束。本项目至少有两项:报名人数不超过活动容量;每一位签到者都仍已报名。签到前守卫能防止未报名者加入签到集合,但取消报名时若只从报名集合删除,仍会留下“已签到却未报名”的孤儿状态。
registrations = {"20260001", "20260002"}
checkins = {"20260001"}
student_id = "20260001"
if student_id in registrations:
registrations.discard(student_id)
checkins.discard(student_id)
print(sorted(registrations))
print(sorted(checkins))
print(2 - len(registrations))['20260002'] [] 1
成功取消释放一个名额。若同一学号随后再次报名,它只回到报名集合,不会自动恢复签到;必须重新选择签到。统计显示两个人数还不够,验收时要核对具体学号,确认没有签到者落在报名集合之外。
报名 A → 签到 A → 取消 A → 统计。若签到集合仍有 A,程序就在第三步破坏关系;如果只测“报名 A → 取消 A”,这个缺陷不会暴露。跨功能错误要用连续操作序列找。
8.5 完成个人 ClubHub M2 基础版
完整示例沿用第 7 章四位虚构成员与容量 2 的活动。循环外建立唯一状态,循环内逐项分支;退出后再用相同数据生成最终汇总。本周仍不定义函数、不读写文件,目的是先把菜单契约和状态变化验收清楚。
# 第8章:个人 M2 活动管理基础版;本章还不定义函数。
# 教学虚构数据;学号是字符串,同名不等于同一人。
members = {
"20260001": "陈晨",
"20260002": "陈晨",
"20260003": "王宁",
"20260004": "李悦",
}
activity = {"code": "EV-0021", "name": "Python体验营", "capacity": 2}
registrations = set()
checkins = set()
while True:
print("\n1 查询活动 | 2 报名 | 3 取消 | 4 签到 | 5 统计 | 0 退出")
choice = input("请选择:").strip()
if choice == "0":
print("已退出,以下是最终汇总")
break
elif choice == "1":
for key, value in activity.items():
print(f"{key}:{value}")
print(f"剩余名额:{activity['capacity'] - len(registrations)}")
elif choice == "2":
student_id = input("报名学号:").strip()
if student_id not in members:
print("报名失败:学号不存在")
elif student_id in registrations:
print("报名失败:请勿重复报名")
elif len(registrations) >= activity["capacity"]:
print("报名失败:名额已满")
else:
registrations.add(student_id)
print(f"报名成功:{student_id} {members[student_id]}")
elif choice == "3":
student_id = input("取消学号:").strip()
if student_id not in members:
print("取消失败:学号不存在")
elif student_id not in registrations:
print("取消失败:尚未报名")
else:
registrations.discard(student_id)
checkins.discard(student_id)
print(f"取消成功:{student_id},签到同步撤销")
elif choice == "4":
student_id = input("签到学号:").strip()
if student_id not in members:
print("签到失败:学号不存在")
elif student_id not in registrations:
print("签到失败:请先报名")
elif student_id in checkins:
print("签到失败:请勿重复签到")
else:
checkins.add(student_id)
print(f"签到成功:{student_id} {members[student_id]}")
elif choice == "5":
print(f"报名人数:{len(registrations)}")
print(f"签到人数:{len(checkins)}")
print(f"剩余名额:{activity['capacity'] - len(registrations)}")
print(f"报名学号:{sorted(registrations)}")
print(f"签到学号:{sorted(checkins)}")
else:
print("菜单无效,请输入0~5")
print(f"报名人数:{len(registrations)}")
print(f"签到人数:{len(checkins)}")
print(f"剩余名额:{activity['capacity'] - len(registrations)}")
print(f"报名学号:{sorted(registrations)}")
print(f"签到学号:{sorted(checkins)}")先用输入 0 检查直接退出:最终应是报名 0、签到 0、剩余 2、两份空学号列表。再按“报名 01 → 签到 01 → 取消 01 → 统计 → 退出”检查同步撤销。此处 01 只是阅读缩写,实际输入完整学号 20260001。在线运行按钮只复制代码并打开指定的 W3Cschool 标签页,需要自行粘贴并按提示输入。
8.6 用逐步状态表解释程序
以下操作序列比只看最终人数更有诊断力:两位同名陈晨分别报名;第一位签到后取消;再让第三位报名。每一步都记录两个集合和剩余名额,遇到输出不符时就能定位第一步出错的位置。
| 操作后 | 报名学号 | 签到学号 | 剩余 |
|---|---|---|---|
| 初始 | 空 | 空 | 2 |
| 01 报名、02 报名 | 01、02 | 空 | 0 |
| 01 签到 | 01、02 | 01 | 0 |
| 01 取消 | 02 | 空 | 1 |
| 03 报名 | 02、03 | 空 | 0 |
01、02、03 分别指三个完整学号。len(checkins) <= len(registrations) 只是必要条件,不足以证明关系正确;真正要检查的是每一个签到学号是否仍在报名集合里。若错在“01 取消”之后,就重点检查取消分支有没有同步 discard()。
8.7 十组黑盒操作序列
每组都从初始空集合重新启动。表中数字 01—04 是学号末两位的阅读缩写,输入时必须使用完整八位学号;“未知”输入 99999999,“空”表示菜单处直接回车。箭头代表下一次 input(),菜单 1、5、0 不再读取学号。
| 组 | 菜单与学号输入顺序 | 最终状态及重点 |
|---|---|---|
| A | 0 | 报名 0、签到 0、剩余 2;两集合空 |
| B | 2→01→2→02→2→03→0 | 报名 01/02;03 报满额;剩余 0 |
| C | 2→01→2→01→4→01→4→01→0 | 重复报名、签到分别被拒;两集合都仅 01 |
| D | 2→未知→4→02→0 | 未知、未报名签到被拒;两集合空 |
| E | 2→01→4→01→3→01→5→0 | 签到后取消;两集合空;剩余 2 |
| F | 2→01→2→02→3→01→2→03→0 | 取消释放名额;最终报名 02/03 |
| G | 3→01→3→未知→0 | “尚未报名”与“学号不存在”分别提示 |
| H | x→空→6→0 | 三次无效菜单,不额外读取学号 |
| I | 2→01→1→5→1→5→0 | 查询统计只读;报名仍仅 01 |
| J | “ 2 ”→“ 01 ”→4→01→3→01→2→01→4→01→0 | 清洗生效;重报后须重新签到;两集合仅 01 |
J 的空格应加在完整学号两侧。除了最终状态,还要核对 B 的满额原因、C 的两次重复原因、D 的未知与未报名原因、G 的两个不同取消原因。基础版 H 中的 6 是非法菜单;现场修改版会增加菜单 6,不能把两版的 H 混为同一验收结果。
8.8 练习、个人 M2 验收与现场修改
先预测,再揭示
- 取消与重报
报名 A、签到 A、取消 A、再报名 A 后,A 已签到吗?
查看答案
没有。取消同时撤销签到;再次报名只加入报名集合,需重新签到。
- 只看数量的陷阱
报名集合只有 A,签到集合只有 B,两边都是 1 人,能判为正确吗?
查看答案
不能。B 未在报名集合中,违反“签到者必已报名”。
- 无效输入
菜单输入
q或6,基础版是否应读取一个学号?查看答案
不读取,只报“菜单无效,请输入0~5”并回到菜单。
独立提交并解释个人活动管理版
每位学生保留自己的源文件、十组操作序列证据和现场修改版本。临时同伴仅做黑盒互测,不能替代个人编码、修复或讲解。
基础达标与提交
- 提交
w08_学号_clubhub_m2.py:六个菜单入口齐全,0 退出、无效项继续,状态只初始化一次。 - 按契约处理未知、重复、满额、未报名、重复签到;取消成功时同时撤销两集合中的学号。
- 提交 A—J 十组“预期—实际—是否通过—修正后复测”;至少让同伴黑盒互测 E、F、H。
- 先独立完成纸面追踪:报名 A、报名 B、签到 A、取消 A、再次报名 A,写出两集合、人数、剩余名额和 A 是否需重签。
- 15 分钟现场修改另存
w08_学号_clubhub_m2_exam.py:新增菜单 6“未签到名单”,用排序后的报名学号逐个判断是否未签到,输出学号、姓名与人数;同步修改菜单提示为 0~6。 - 修改版另测空名单、报名 A/B 但只签到 A、A/B 都签到;提交 100—200 字 M2 复盘,说明一处跨功能错误和一个已知限制。
这份演示代码是阅读和练习参照,个人验收看自己的代码、测试记录与现场修改。第 9 章再从重复的菜单分支识别函数边界;本章不借助自定义函数、异常处理、文件或多活动嵌套规避状态问题。
章末检查
- 我知道哪些操作只读,哪些操作会修改两个集合。
- 我能用一条最短操作序列暴露“取消未同步签到”的缺陷。
- 我会核对具体学号关系,而不只看报名、签到人数。
- 我能独立运行十组测试并解释个人 M2 程序的已知限制。
延伸阅读:廖雪峰《Python教程》在线 PDF中的“条件判断”“循环”“dict 和 set”,以及Python 官方教程:数据结构。本章业务契约与测试顺序按课程项目制定。