第 4 周:字典、集合与数据组织
当数据需要“按名字查值”“统计次数”或“按类别分组”时,列表不再是最合适的结构。本周学习用字典保存键值对应关系,用集合去重和做成员判断,用 zip() 把成组数据配对。
建议用时:8-10 小时。
本周目标#
学完本周,你应该能够:
- 创建、访问、增加、修改和删除字典元素。
- 区分
d[key]与d.get(key)。 - 遍历字典的键、值和键值对。
- 使用字典完成计数、分组和映射。
- 使用集合去重和判断成员。
- 理解集合无序,不依赖其输出顺序。
- 使用
zip()合并多个序列,并处理长度不一致。 - 在一组学生记录上完成排序和按班级汇总。
4.1 字典是什么#
字典保存“键到值”的映射。键用来查找,值是与键对应的数据。
student = {
"student_id": "S001",
"name": "张晨",
"score": 88,
}
print(student["name"]) # 张晨
print(student["score"]) # 88
结构规则:
- 外层使用花括号
{}。 - 每个元素写成
键: 值。 - 元素之间用逗号分隔。
- 键通常使用字符串或数字。
- 同一个字典中的键不能重复。
- 值的类型可以相同,也可以不同。
如果重复写同一个键,后面的值会覆盖前面的值:
data = {"name": "张晨", "name": "李雨"}
print(data) # {'name': '李雨'}
4.2 访问、增加和修改#
student = {"name": "张晨", "score": 82}
print(student["name"])
student["score"] = 88 # 键已存在,修改值
student["class_name"] = "高一(1)班" # 键不存在,增加新键值对
print(student)
# {'name': '张晨', 'score': 88, 'class_name': '高一(1)班'}
使用 [] 读取不存在的键会报 KeyError:
# print(student["phone"])
4.3 get():安全读取#
student = {"name": "张晨", "score": 82}
print(student.get("name")) # 张晨
print(student.get("phone")) # None
print(student.get("phone", "未填写")) # 未填写
get() 在键不存在时不会报错,而是返回默认值。第二个参数省略时默认是 None。
注意:get() 只负责读取,不会自动把默认值写回字典:
student.get("phone", "未填写")
print("phone" in student) # False
如果希望新增键,需要显式赋值:
student["phone"] = "未填写"
4.4 删除元素#
student = {"name": "张晨", "score": 82, "phone": "123"}
score = student.pop("score")
print(score) # 82
print(student) # {'name': '张晨', 'phone': '123'}
del student["phone"]
print(student) # {'name': '张晨'}
pop() 返回被删除的值。若键不存在,它会报 KeyError,可以给默认值避免异常:
phone = student.pop("phone", "没有该字段")
4.5 字典遍历#
scores = {"语文": 88, "数学": 95, "英语": 80}
for subject in scores:
print(subject)
直接遍历字典时,得到的是键。
同时访问键和值:
for subject, score in scores.items():
print(subject, score)
只遍历键:
for subject in scores.keys():
print(subject)
只遍历值:
for score in scores.values():
print(score)
keys()、values()、items() 返回的是动态视图。修改字典后,再遍历这些视图会看到新的内容。基础题中主要掌握它们可以用于 for 和 list()。
4.6 用字典做计数#
统计每个单词出现次数:
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counts = {}
for word in words:
if word in counts:
counts[word] += 1
else:
counts[word] = 1
print(counts)
# {'apple': 3, 'banana': 2, 'orange': 1}
也可以用 get() 简化:
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
counts.get(word, 0) 表示“如果已有这个词,取当前次数;否则从 0 开始”,加 1 后再保存回字典。
如果直接写:
# counts[word] += 1
第一次遇到新词时会报 KeyError,因为键还不存在。
4.7 用字典做分组#
把学生姓名按班级分组:
students = [
("高一(1)班", "张晨"),
("高一(2)班", "李雨"),
("高一(1)班", "王浩"),
("高一(2)班", "陈欣"),
]
groups = {}
for class_name, name in students:
if class_name not in groups:
groups[class_name] = []
groups[class_name].append(name)
print(groups)
结果:
{
"高一(1)班": ["张晨", "王浩"],
"高一(2)班": ["李雨", "陈欣"],
}
分组时第一次遇到某个键,必须先创建空列表,再追加数据。否则 groups[class_name].append(...) 会因为列表还不存在而报错。
4.8 字典保存复杂记录#
一个学生有多项字段时,可以用字典:
students = [
{"student_id": "S001", "name": "张晨", "score": 88},
{"student_id": "S002", "name": "李雨", "score": 95},
{"student_id": "S003", "name": "王浩", "score": 72},
]
for student in students:
print(
student["student_id"],
student["name"],
student["score"],
)
按分数从高到低排序:
ranked = sorted(
students,
key=lambda student: student["score"],
reverse=True,
)
for student in ranked:
print(student["name"], student["score"])
key=lambda student: student["score"] 的意思是:排序时不直接比较整个字典,而是先对每条记录取出 score 作为排序依据。lambda 可以理解成一个临时的小函数。
并列排序的规则虽然还没要求全面掌握,但要知道:如果两个分数相同,当前 sorted 会保留它们在原列表中的相对顺序。
4.9 zip():把多个序列配对#
names = ["张晨", "李雨", "王浩"]
scores = [88, 95, 72]
pairs = list(zip(names, scores))
print(pairs)
# [('张晨', 88), ('李雨', 95), ('王浩', 72)]
在循环中逐个解包:
for name, score in zip(names, scores):
print(f"{name}: {score}")
构建字典:
score_map = dict(zip(names, scores))
print(score_map) # {'张晨': 88, '李雨': 95, '王浩': 72}
zip() 在最短的序列结束时停止:
print(list(zip([1, 2, 3], ["A", "B"])))
# [(1, 'A'), (2, 'B')]
如果要求所有数据一一对应,应先检查两个序列长度是否相等,而不是默默丢掉多余数据。
第一阶段常见推演#
d = {"a": 1, "b": 2}
print(list(zip(d.keys(), d.values())))
结果是:
[("a", 1), ("b", 2)]
zip() 把第一个键与第一个值、第二个键与第二个值分别配对,而不是把键合成一个列表、值合成另一个列表。
4.10 集合#
集合用花括号表示,保存不重复元素:
numbers = {1, 2, 2, 3, 3, 4}
print(numbers) # {1, 2, 3, 4}
创建空集合必须使用 set():
empty_set = set()
print(type(empty_set).__name__) # set
empty_dict = {}
print(type(empty_dict).__name__) # dict
{} 创建的是空字典,不是空集合。
去重#
names = ["张晨", "李雨", "张晨", "王浩", "李雨"]
unique_names = list(set(names))
print(unique_names)
数学上集合没有固定顺序,所以 unique_names 的元素先后顺序不应作为程序结果来依赖。正式题若要求“保持原顺序去重”,可以使用字典:
unique_names = list(dict.fromkeys(names))
print(unique_names) # ['张晨', '李雨', '王浩']
从 Python 3.7 起,字典会保留键的插入顺序,因此这种写法同时完成“去重”和“保留首次出现顺序”。目标环境是 Python 3.11,可以这样使用。
如果需要按值排序:
unique_sorted = sorted(set(names))
先让集合完成去重,再用 sorted() 明确排序,不依赖集合自己的显示顺序。
成员判断#
valid_levels = {"优秀", "良好", "及格", "不及格"}
level = "良好"
print(level in valid_levels) # True
集合的成员判断通常比列表更快,因为集合可以直接按值定位,而不必从头逐个查找。初学阶段先掌握语义,不必过早优化所有代码。
集合运算#
python_group = {"张晨", "李雨", "王浩"}
excel_group = {"李雨", "王浩", "陈欣"}
print(python_group & excel_group) # 交集
print(python_group | excel_group) # 并集
print(python_group - excel_group) # 差集
三种结果分别表示“两组都参加”“至少参加一组”“只参加第一组”。
集合元素必须可哈希#
列表和字典不能直接放进集合,因为它们可以修改:
# invalid = {[1, 2], [3, 4]} # TypeError
valid = {(1, 2), (3, 4)}
元组不可修改,在元素本身也合适时可以放进集合。
4.11 选择合适的结构#
| 需求 | 推荐结构 | 原因 |
|---|---|---|
| 保持一排有顺序的数据 | 列表 | 可修改、可索引 |
| 用一个键查一个值 | 字典 | 按关键字直接映射 |
| 只关心有哪些不重复元素 | 集合 | 自动去重、成员判断方便 |
| 保存不可修改的一组值 | 元组 | 结构稳定 |
| 一个学生有多个字段 | 字典或对象 | 字段名称明确 |
| 多条学生记录 | 字典列表 | 每条记录结构相同,便于遍历和排序 |
选择结构时先问“我要怎样取数据”,再看哪种结构最直接。
4.12 本周作品一:通讯录#
contacts = {}
while True:
command = input("add 姓名 电话 / get 姓名 / delete 姓名 / list / quit:")
parts = command.split()
if not parts:
continue
action = parts[0]
if action == "quit":
break
elif action == "add" and len(parts) == 3:
name, phone = parts[1], parts[2]
contacts[name] = phone
print("已保存:", name)
elif action == "get" and len(parts) == 2:
name = parts[1]
print(contacts.get(name, "联系人不存在"))
elif action == "delete" and len(parts) == 2:
name = parts[1]
if name in contacts:
del contacts[name]
print("已删除:", name)
else:
print("联系人不存在")
elif action == "list":
for name, phone in contacts.items():
print(name, phone)
else:
print("命令格式不正确")
重点检查:
- 同名联系人再次
add时会更新电话,而不是出现两个同名键。 - 查询不存在的联系人时给出提示。
- 删除前先用
in判断,避免KeyError。
4.13 本周作品二:投票统计#
votes = ["A", "B", "A", "C", "A", "B"]
counts = {}
for candidate in votes:
counts[candidate] = counts.get(candidate, 0) + 1
print("得票:", counts)
winner = None
highest = -1
for candidate, count in counts.items():
if count > highest:
highest = count
winner = candidate
print("最高票:", winner, highest)
这里使用严格大于号 >,所以出现并列最高票时,程序保留字典中先出现的候选人。题目若规定其他破同分规则,必须按题目修改。
4.14 本周作品三:按班级统计成绩#
students = [
{"class_name": "高一(1)班", "score": 88},
{"class_name": "高一(2)班", "score": 90},
{"class_name": "高一(1)班", "score": 76},
{"class_name": "高一(2)班", "score": 82},
]
class_scores = {}
for student in students:
class_name = student["class_name"]
score = student["score"]
if class_name not in class_scores:
class_scores[class_name] = []
class_scores[class_name].append(score)
for class_name, scores in class_scores.items():
average = sum(scores) / len(scores)
print(f"{class_name} 平均分:{average:.2f}")
输出:
高一(1)班 平均分:82.00
高一(2)班 平均分:86.00
这个“先分组、再对每组计算”的模式会反复出现在阶段二和阶段三。阶段三还会把结果写入 Excel。
4.15 独立练习#
A. 快速判断题#
d = {"a": 1}; print(d["b"])会发生什么?d.get("b")返回什么?d.get("b", 0)返回什么?下面两行分别得到什么类型?
a = {} b = set()list(zip([1, 2], ["A", "B"]))是什么?zip([1, 2, 3], ["A", "B"])会配出几对?{"a": 1, "b": 2}.items()遍历时每项包含什么?集合中的元素可以有列表吗?
list(set([3, 1, 3, 2]))的结果一定要按[1, 2, 3]输出吗?要按原顺序去重,哪种结构更合适?
B. 编程题#
- 输入一串英文单词,统计每个单词出现次数。
- 有一个姓名列表,输出去重后的姓名,并保持首次出现顺序。
- 有两个列表
names和scores,用zip()输出每名学生的分数。 - 用字典保存 5 个国家的首都,并让用户输入国家查询首都。
- 统计一段文字中每个字符出现的次数,忽略空格。
- 把学生记录列表按分数降序排序,输出姓名和分数。
- 按班级汇总学生总分、人数和平均分。
- 从两个名字集合中输出同时参加两个活动的学生,以及只参加第一个活动的学生。
4.16 练习答案与解释#
A. 快速判断题答案#
- 报
KeyError,因为键"b"不存在。 None。0。a是空字典dict,b是空集合set。[(1, "A"), (2, "B")]。- 两对,
zip()到最短序列结束。 - 每项是一个“键、值”二元组。
- 不可以,列表可修改且不可哈希,会报
TypeError。 - 不一定。集合本身不保证按这个顺序呈现,需要排序时必须显式
sorted()。 - 字典,可使用
dict.fromkeys()并保留插入顺序。
B. 编程题参考答案#
单词计数:
words = input().split() counts = {} for word in words: counts[word] = counts.get(word, 0) + 1 print(counts)保持首次出现顺序去重:
names = input().split() unique_names = list(dict.fromkeys(names)) print(unique_names)配对姓名和分数:
names = ["张晨", "李雨", "王浩"] scores = [88, 95, 72] for name, score in zip(names, scores): print(name, score)国家首都查询:
capitals = { "中国": "北京", "日本": "东京", "法国": "巴黎", "德国": "柏林", "意大利": "罗马", } country = input() print(capitals.get(country, "没有该国家的记录"))字符计数:
text = input() counts = {} for char in text: if char == " ": continue counts[char] = counts.get(char, 0) + 1 print(counts)按分数排序:
students = [ {"name": "张晨", "score": 88}, {"name": "李雨", "score": 95}, {"name": "王浩", "score": 72}, ] ranked = sorted( students, key=lambda student: student["score"], reverse=True, ) for student in ranked: print(student["name"], student["score"])按班级汇总:
students = [ {"class_name": "高一(1)班", "score": 88}, {"class_name": "高一(2)班", "score": 90}, {"class_name": "高一(1)班", "score": 76}, ] summary = {} for student in students: class_name = student["class_name"] score = student["score"] if class_name not in summary: summary[class_name] = {"total": 0, "count": 0} summary[class_name]["total"] += score summary[class_name]["count"] += 1 for class_name, data in summary.items(): average = data["total"] / data["count"] print(class_name, data["total"], data["count"], f"{average:.2f}")集合运算:
first = {"张晨", "李雨", "王浩"} second = {"李雨", "王浩", "陈欣"} print("两个都参加:", first & second) print("只参加第一个:", first - second)
4.17 常见错误清单#
- 使用不存在的键访问字典,导致
KeyError。 - 以为
get()会把默认值新增到字典。 - 遍历字典时误以为得到的是值。
- 计数时没有初始化新键。
- 分组时没有先创建空列表。
zip()长度不一致时静默丢失数据。- 依赖集合的显示顺序。
- 把空花括号当成空集合。
- 把列表放入集合或作为字典键。
- 排序时忘了指定按哪个字段排序。
4.18 过关检查#
- 能使用
[]和get()读写字典并解释差异。 - 能遍历键、值和键值对。
- 能独立写出字典计数模式。
- 能独立写出“先创建空列表、再追加”的分组模式。
- 能解释
list(zip(d.keys(), d.values()))的结果。 - 能完成集合去重,并明确是否需要保持原顺序。
- 能用字典列表保存 3 条完整记录。
- 能按某个字段排序并解释
key=lambda的作用。 - 独立完成通讯录和班级汇总。
- 能在不运行代码的情况下推演
zip、字典修改和集合去重结果。