第 7 章 · 从名单走向身份与关系
字典、集合与活动数据
第 6 章用姓名做唯一标识,遇到两位都叫“陈晨”的同学就会误判重复。现在用学号查姓名,用一个字典描述活动,用两个集合分别保存报名与签到学号。关键不只是换了容器,而是每次操作都维护“谁是谁、谁已报名、谁可签到”的规则。
- 预计阅读
- 65 分钟
- 上机实践
- 90 分钟
- 前置知识
- 第 1—6 章
- 本章产出
- 个人活动数据程序
学完本章,你应该能够
- 用字典的键和值表达学号到姓名、字段名到活动信息的映射。
- 完成字典增删改查与遍历,区分直接索引和
get()的结果。 - 正确建立空集合,用
add()、discard()和成员判断管理唯一学号。 - 按“未知 → 重复 → 满额”和“未知 → 未报名 → 已签到”顺序守住两类操作。
- 用具体学号检验签到与报名的一致性,独立完成八组测试和结构选型说明。
7.1 一种数据结构不能回答所有问题
第 6 章的姓名列表能保留顺序,但无法区分同名同学;本章没有报名先后展示要求,重点是身份映射与唯一关系。四位虚构成员中,20260001 和 20260002 都叫陈晨,仍是两个不同的人。学号必须保留为字符串,不把它当作可算术运算的数字。
| 问题 | 选择 | 为什么 |
|---|---|---|
| 学号对应哪位成员? | 字典 members | 用唯一学号作键,查到姓名 |
| 活动有哪些属性? | 字典 activity | 用 code/name/capacity 作字段名 |
| 哪些学号已报名? | 集合 registrations | 同一学号只保留一次 |
| 哪些学号已签到? | 集合 checkins | 身份唯一,且必须先属于报名集合 |
本章固定只有一个免费活动,不做费用计算、多活动嵌套或持久化。集合不保证报名顺序;若要稳定显示,用 sorted() 得到临时列表,但不能把排序后的结果说成报名先后。
7.2 字典:由学号查姓名
字典用花括号包围“键: 值”对。members[student_id] 按键取值,不是列表索引;student_id in members 检查的是键是否存在。直接访问不存在的键会得到 KeyError,因此业务操作应先做成员守卫。
members = {"20260001": "陈晨", "20260002": "陈晨"}
print(members["20260001"])
print("20260002" in members)
print("陈晨" in members)
print(members.get("99999999"))陈晨 True False None
两个键不同,值可以相同。get() 在键不存在时默认返回 None,也可给默认值;它适合安全读取可选信息,但不能因为用了 get() 就跳过“未知学号不能报名”的业务检查。"陈晨" in members 为 False,因为它是值,不是键。
7.3 活动字典的增、删、改、查与遍历
活动字段名是固定的教学约定:code、name、capacity。为某个键赋值,若键已存在就是修改,不存在就是新增。用 del 可删除一个已存在的键;但基础活动记录必须保留三个必需字段,删除示例只在临时字段上做。
activity = {"code": "EV-0021", "name": "Python体验营", "capacity": 2}
activity["capacity"] = 3
activity["note"] = "教学演示"
print(activity.get("venue", "地点未定"))
for key, value in activity.items():
print(f"{key}: {value}")
del activity["note"]
print("note" in activity)地点未定 code: EV-0021 name: Python体验营 capacity: 3 note: 教学演示 False
items() 让每一轮同时得到键和值。这里为了展示“修改”把容量临时改成 3;后面的正式程序仍按预置规则使用容量 2。字典内容一旦修改,后续读取就会看到新值;不要把示例修改偷偷带入八组固定测试。
7.4 集合:同一学号只能出现一次
空集合必须写 set();空花括号 {} 是空字典。集合的 add() 用于加入,discard() 用于删除,in 用于成员判断。集合本身会去重,但业务程序仍要在重复报名时明确提示,不能只让 add() 静默无事。
registrations = set()
registrations.add("20260001")
registrations.add("20260001")
registrations.add("20260002")
print(len(registrations))
print("20260002" in registrations)
registrations.discard("99999999")
print(sorted(registrations))2 True ['20260001', '20260002']
discard() 删除不存在的值不会报错;但“没有报错”不等于“取消成功”。若要向学生报告操作结果,仍要先判断对象是否已报名。第 8 章会用它处理取消并同步撤销签到。本章只用 add() 完成报名和签到。
7.5 签到不是另一份随意增长的名单
报名集合与签到集合都存学号,但含义不同:签到者必须先在报名集合中。每次成功签到后,checkins 中的每个学号都应仍在 registrations 中。若只看两个集合的长度,无法发现“签到的是另一个人”这种关系错误。
registrations = {"20260001"}
checkins = set()
student_id = "20260002"
if student_id not in registrations:
print("签到失败:请先报名")
else:
checkins.add(student_id)
print(sorted(checkins))签到失败:请先报名 []
完整报名判断的顺序是“学号不在成员字典 → 已在报名集合 → 名额已满 → 成功加入”;重复应先于满额检查,这样即使已满,也能准确回答“请勿重复报名”。签到判断是“未知学号 → 未报名 → 已签到 → 成功加入”。所有拒绝路径都不改变两个集合。
若报名集合是 {"20260001"},签到集合却是 {"20260002"},两边人数都是 1,仍然是错误状态。要逐个检查签到学号是否已经报名,而不是仅比较人数大小。
7.6 完成活动字典与报名签到程序
正式程序使用四位虚构成员、一个容量 2 的活动,固定读取三次报名和两次签到。每轮都读取一个学号,包括已满额时的第三次报名;否则无法给重复或未知学号提供准确反馈。输入先 strip(),空白与未知学号统一按“不存在”处理。
# 第7章:活动字典与报名、签到集合
# 教学虚构数据;学号是字符串,同名不等于同一人。
members = {
"20260001": "陈晨",
"20260002": "陈晨",
"20260003": "王宁",
"20260004": "李悦",
}
activity = {"code": "EV-0021", "name": "Python体验营", "capacity": 2}
registrations = set()
checkins = set()
print(f"活动:{activity['code']} {activity['name']}")
for sequence in range(3):
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]}")
for sequence in range(2):
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]}")
print(f"报名人数:{len(registrations)}")
print(f"签到人数:{len(checkins)}")
print(f"剩余名额:{activity['capacity'] - len(registrations)}")
print(f"报名学号:{sorted(registrations)}")
print(f"签到学号:{sorted(checkins)}")例如报名依次输入 20260001、20260002、20260003,签到依次输入前两位学号:两位同名陈晨都能报名,第三位因名额已满失败;最终报名 2、签到 2、剩余 0。两集合按学号排序输出,仅为了结果稳定,不表达报名先后。
- 1先确认身份
学号不存在时不访问
members[student_id],避免KeyError。 - 2成功路径才更新
只有全部守卫通过才调用
add(),拒绝路径不改状态。 - 3从集合读出统计
报名、签到人数分别由
len()得到,剩余名额由容量减报名人数得到。
7.7 八组规则测试:每组重新运行
表中每组都输入五次:前三次是报名,后两次是签到;“回车”表示空输入。先预测完整最终状态,再运行并记录实际提示和两个具体学号集合。
| 组 | 三次报名 → 两次签到 | 关键预期 |
|---|---|---|
| A | 01、02、03 → 01、02 | 两位陈晨均成功;03 满额;报名 2、签到 2 |
| B | 01、01、03 → 01、01 | 重复报名、重复签到各被拒;签到 1 |
| C | 未知、01、01 → 02、01 | 未知被拒;02 未报名;报名 1、签到 1 |
| D | 回车、回车、回车 → 回车、回车 | 两集合为空;剩余名额 2 |
| E | “ 01 ”、02、03 → “ 01 ”、02 | 首尾空白被清洗;报名、签到均 2 |
| F | 01、02、03 → 未知、03 | 未知签到、未报名签到被拒;签到 0 |
| G | 01、02、01 → 01、02 | 满额时第三次重复仍报“请勿重复报名” |
| H | 04、03、04 → 04、03 | 最终显示按学号排序为 03、04 |
表中 01—04 是 20260001—20260004 的阅读缩写,实际输入必须是完整八位学号;“未知”可输入 99999999。若未知学号触发 KeyError,检查字典访问是否早于身份守卫;若重复时错误报满额,检查判断顺序;若集合内容与人数矛盾,检查是否维护了多余的独立计数器。
7.8 练习与个人任务
先预测,再揭示
- 键还是值
两位陈晨在字典中是重复键吗?
"陈晨" in members会得到什么?查看答案
不是。键是不同的学号,值可以相同;姓名不在键中,结果为
False。 - 空容器
{}与set()分别创建什么?查看答案
前者是空字典,后者才是空集合。
- 人数与关系
报名为 01,签到为 02,为什么“两个集合人数都是 1”仍不能算通过?
查看答案
签到的具体学号 02 不在报名集合中,违反“先报名后签到”。
独立实现活动数据程序
每位学生维护自己的四份数据并提交源代码、测试表与解释;同伴可以提供测试输入,但不替代个人判断或提交。
最低验收要求
- 提交
w07_学号_activity_sets.py,保留四位虚构成员、一个容量 2 的免费活动、两份初始空集合。 - 固定三次报名、两次签到;即使满额也读完输入,不加 q 口令或菜单。
- 按规定优先级给出拒绝原因;只有成功路径调用
add(),用len()和sorted()汇总。 - 提交 A—H 八组“预期—实际—结论—修正后复测”,说明两位同名者为何都能报名。
- 限时修改另存文件:在汇总后用
for遍历排序后的报名学号,逐个判断不在签到集合者,显示未签到人数与名单;不另存一份长期同步的集合。 - 提交 80—150 字结构选型与错误复盘,解释字典、集合各自解决了什么问题。
本章不引入嵌套字典、推导式、复杂集合运算、自定义函数或文件。第 8 章才把各操作装入持续运行的菜单;现在固定轮次更便于先验证两集合的规则。
章末检查
- 我用学号作键,不拿姓名代替身份。
- 我知道字典
get()与直接索引的差别。 - 我能解释空字典和空集合的写法。
- 我检查签到学号属于报名集合,而非只比较人数。
延伸阅读:廖雪峰《Python教程》在线 PDF中的“dict 和 set”,以及菜鸟教程 Python 3 字典、Python 3 集合。项目规则以本章的虚构数据和课程主数据为准。