跳到主要内容
Python 程序设计 · 在线教材适用于 2026—2027 学年第一学期

第 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 内,每次回到菜单都会重置为空,上一轮成功报名就“消失”。正确做法是循环外初始化,循环内按选项读取或更新。

menu_skeleton.py
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 状态不变量:取消必须同步撤销签到

“不变量”是每一次操作前后都必须成立的约束。本项目至少有两项:报名人数不超过活动容量;每一位签到者都仍已报名。签到前守卫能防止未报名者加入签到集合,但取消报名时若只从报名集合删除,仍会留下“已签到却未报名”的孤儿状态。

cancel_both_sets.py
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 的活动。循环外建立唯一状态,循环内逐项分支;退出后再用相同数据生成最终汇总。本周仍不定义函数、不读写文件,目的是先把菜单契约和状态变化验收清楚。

clubhub_m2.py
# 第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、02010
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 不再读取学号。

组菜单与学号输入顺序最终状态及重点
A0报名 0、签到 0、剩余 2;两集合空
B2→01→2→02→2→03→0报名 01/02;03 报满额;剩余 0
C2→01→2→01→4→01→4→01→0重复报名、签到分别被拒;两集合都仅 01
D2→未知→4→02→0未知、未报名签到被拒;两集合空
E2→01→4→01→3→01→5→0签到后取消;两集合空;剩余 2
F2→01→2→02→3→01→2→03→0取消释放名额;最终报名 02/03
G3→01→3→未知→0“尚未报名”与“学号不存在”分别提示
Hx→空→6→0三次无效菜单,不额外读取学号
I2→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 验收与现场修改

先预测,再揭示

  1. 取消与重报

    报名 A、签到 A、取消 A、再报名 A 后,A 已签到吗?

    查看答案

    没有。取消同时撤销签到;再次报名只加入报名集合,需重新签到。

  2. 只看数量的陷阱

    报名集合只有 A,签到集合只有 B,两边都是 1 人,能判为正确吗?

    查看答案

    不能。B 未在报名集合中,违反“签到者必已报名”。

  3. 无效输入

    菜单输入 q 或 6,基础版是否应读取一个学号?

    查看答案

    不读取,只报“菜单无效,请输入0~5”并回到菜单。

第 8 周 · 个人里程碑 M2

独立提交并解释个人活动管理版

每位学生保留自己的源文件、十组操作序列证据和现场修改版本。临时同伴仅做黑盒互测,不能替代个人编码、修复或讲解。

基础达标与提交

  • 提交 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 复盘,说明一处跨功能错误和一个已知限制。
下载 M2 基础版演示代码

这份演示代码是阅读和练习参照,个人验收看自己的代码、测试记录与现场修改。第 9 章再从重复的菜单分支识别函数边界;本章不借助自定义函数、异常处理、文件或多活动嵌套规避状态问题。

章末检查

  • 我知道哪些操作只读,哪些操作会修改两个集合。
  • 我能用一条最短操作序列暴露“取消未同步签到”的缺陷。
  • 我会核对具体学号关系,而不只看报名、签到人数。
  • 我能独立运行十组测试并解释个人 M2 程序的已知限制。

延伸阅读:廖雪峰《Python教程》在线 PDF中的“条件判断”“循环”“dict 和 set”,以及Python 官方教程:数据结构。本章业务契约与测试顺序按课程项目制定。