PY-04 组合数据类型
04 · 组合数据类型(Data Structures)
📅 预计 100 分钟 | ⭐ = 最高频考点章
✍️ 本讲配套练习:本地python grader.py 04,或网页练习
4.0 为什么要学组合数据类型
前几章里,一个变量只能存一个值:x = 5、name = "小明"。但现实中的数据从来不是孤零零一个——你有一整架书、一张全班成绩表、一堆要找出重复的编号。组合数据类型(也叫数据结构)就是 Python 提供的一批「一次装下很多东西」的容器,本讲四兄弟:列表、元组、字典、集合。
先记住四个生活比喻,后面所有知识点都挂在这四个钩子上,忘不掉:
| 结构 | 生活比喻 | 一句话概括 |
|---|---|---|
列表 list |
书架 | 一格一格按顺序摆,可加、可拿、可换 |
元组 tuple |
只读信封 | 封好的信,不能拆改,只能整体传阅 |
字典 dict |
查表 | 名字 → 电话,查号台,按 key 秒定位 |
集合 set |
去重袋 | 丢进去自动去重,还能做交并差 |
4.1 四种结构总览 ⭐
| 类型 | 符号 | 有序 | 可变 | 允许重复 | 场景 |
|---|---|---|---|---|---|
列表 list |
[1,2] |
✅ | ✅ | ✅ | 最常用,默认选项 |
元组 tuple |
(1,2) |
✅ | ❌ | ✅ | 只读数据、字典键 |
字典 dict |
{"a":1} |
✅(3.7+) | ✅ | 键不重复 | 键值映射、快速查找 |
集合 set |
{1,2} |
❌ | ✅ | ❌(自动去重) | 去重、关系运算 |
怎么记?三句话:
- 列表是默认选项——九成场景先想它。
- 要只读、要当字典键 → 选元组。
- 查得快用字典,去重/求关系用集合。
⚠️ 常见错误:把四种结构混为一谈
{}是空字典,不是空集合;空集合必须写set()。- 列表和元组有序,字典的顺序是 3.7 版之后才有保证的插入序,而集合完全无序——别指望
print({3,1,2})一定按你写的顺序输出。 - 四种结构用同样的方括号下标取元素(
lst[0]、t[0]、d["key"]、集合不行),但语义完全不同。
4.2 列表 list ⭐
列表就是书架:有顺序,能一本本往上面摆,也能抽出来换掉。它是 Python 里最常用的结构,几乎所有批量数据都先用列表装。
# 书架从空开始,一本本摆上去
books = []
books.append("Python") # append 末尾加 → ['Python']
books.append("算法") # → ['Python', '算法']
books.insert(0, "C语言") # insert(位置, 值) 插到开头 → ['C语言', 'Python', '算法']
books.extend(["数据库", "OS"]) # extend 末尾接一整排 → ['C语言','Python','算法','数据库','OS']
增、删、改、查——列表四大件:
nums = [10, 20, 30]
x = nums.pop() # pop() 取走末尾并返回 → x=30, nums=[10, 20]
y = nums.pop(0) # pop(索引) 取走指定位置 → y=10, nums=[20]
nums.remove(20) # remove(值) 删掉第一个匹配的 → nums=[]
# 注意:pop 按「位置」取,remove 按「值」删,别混了
排序与反转:
nums = [3, 1, 2]
nums.sort() # 原地升序 → [1, 2, 3](改的是自己,返回 None)
nums.reverse() # 原地反转 → [3, 2, 1]
new = sorted(nums) # sorted 返回新列表,nums 不变 ⭐
切片(书架抽其中一段)⭐:格式 lst[开始:结束:步长],左闭右开——含开始,不含结束。
nums = [10, 20, 30, 40, 50]
print(nums[1:3]) # [20, 30] 取索引 1、2,不含 3
print(nums[:3]) # [10, 20, 30] 开头省略=从头
print(nums[2:]) # [30, 40, 50] 结尾省略=到末尾
print(nums[::-1]) # [50, 40, 30, 20, 10] 反转 ⭐
print(nums[::2]) # [10, 30, 50] 隔一个取一个
print(nums[-1]) # 50,负数索引从尾部数,-1 是最后一个
遍历:
for x in nums: # 遍历值
for i, x in enumerate(nums): # 同时拿索引 i 和值 x(考试常考)
列表推导式 ⭐(一条语句批量生成列表,本讲第一个重头戏):
[x * 2 for x in range(5)] # [0, 2, 4, 6, 8]
[x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8] 带条件的推导式(高频)
⚠️ 常见错误(列表)
- 索引越界:只有 3 个元素却写
nums[5],直接IndexError崩溃;但切片越界不报错,只给你能取到的部分。 sort()和sorted()搞混:sort()原地改、返回None;sorted()返回新列表。写nums = nums.sort()会把列表变成None,经典大坑。- 复制还是别名:
b = a不是复制,是给同一份数据起第二个名字(详见 4.6)。 remove删的是第一个匹配的值,且值不存在会ValueError;pop(空列表)会IndexError。
4.3 元组 tuple
元组就是只读信封:封好口,能看里面、能整封信递给人,但不能拆开改。写法是圆括号:
t = (1, 2, 3)
print(t[0]) # 1,能读
# t[0] = 99 ❌ TypeError: tuple 不可变,不能改
单元素陷阱 ⭐(考试最爱挖的坑):
t1 = (5) # 这只是整数 5,不是元组!
t2 = (5,) # 加一个逗号,才是元组
print(type(t1)) # <class 'int'>
print(type(t2)) # <class 'tuple'>
解包 unpack ⭐:把元组(或列表)里的元素一次取出来分给多个变量:
a, b, c = (1, 2, 3) # 数量必须一一对应
print(a, b, c) # 1 2 3
first, *rest = [1, 2, 3, 4] # 星号解包:first=1, rest=[2, 3, 4]
first, *mid, last = [1, 2, 3, 4] # first=1, mid=[2, 3], last=4
交换两个变量——元组解包最经典的用法 ⭐:
a, b = 1, 2
a, b = b, a # 右边先打包成元组 (2, 1),再解包给左边
print(a, b) # 2 1
元组到底有什么用?
- 函数返回多个值时,实际返回的是一个元组(
return a, b就是返回(a, b))。 - 元组不可变 → 可哈希,所以能当字典的键(列表可变,不能当键,见 4.4)。
⚠️ 常见错误(元组)
- 忘了单元素加逗号:
t = (5)结果是整数,不是元组。 - 解包数量不匹配:
a, b = (1, 2, 3)会ValueError: too many values to unpack。 - 以为元组能像列表一样
append/sort——元组没有任何修改方法,别想改它。
4.4 字典 dict ⭐
字典就是查表(查号台):给你一个「键」,立刻返回对应的「值」。跟列表按位置找不同,字典按名字找,速度快得多。
# 查号台:名字 → 电话
phone = {"小明": "138", "小红": "139"}
phone["小刚"] = "137" # 新增键
phone["小明"] = "136" # 修改已有键的值
print(phone["小明"]) # 136,按键取值
del phone["小红"] # 删除一个键值对
安全取值 get ⭐(比直接下标安全一百倍):
phone = {"小明": "138"}
print(phone["小明"]) # 138
# print(phone["小红"]) ❌ KeyError:键不存在直接崩
print(phone.get("小红")) # None,优雅处理,不报错
print(phone.get("小红", "无")) # "无",指定默认值
遍历 ⭐:
for k in phone: # 默认遍历的是键
for k, v in phone.items(): # 同时拿键和值(最常用)⭐
for v in phone.values(): # 只拿值
for k in phone.keys(): # 只拿键
判断键是否存在:if "小明" in phone: ——注意,对字典来说 in 判断的是键,不是值。
setdefault 分组 ⭐(get 的升级版:键不存在就先塞个默认值):
d = {}
d.setdefault("数学", []).append(90) # key 不存在 → 先建空列表再 append
d.setdefault("数学", []).append(95) # key 已存在 → 直接 append
print(d) # {'数学': [90, 95]}
字典推导式:
{x: x * x for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
⭐ 词频统计经典套路——本讲第二重头戏,大题常考:
count = {}
for w in ["a", "b", "a"]:
count[w] = count.get(w, 0) + 1
print(count) # {'a': 2, 'b': 1}
思路拆解:count.get(w, 0) 先拿到「这个单词目前出现了几次,没有就按 0」,加 1 再存回去。
⚠️ 常见错误(字典)
KeyError:直接d[键]取值而键不存在就会崩;不确定时一律用d.get(键, 默认值)。- 键必须可哈希:数字、字符串、元组可以当键;列表、字典不能当键(
TypeError: unhashable type)。原因:列表可变,哈希值会变,Python 就找不到它了。 in判断的是键不是值:"138" in phone是False。- 遍历时别删键:
for k in d: del d[k]会报错(遍历过程中字典结构变了);要删先list(d.keys())复制一份再遍历。
4.5 集合 set
集合就是去重袋:往里丢东西,重复的自动消失;还能做交并差。用花括号(记住:空花括号 {} 是字典):
s = {1, 2, 2, 3} # 自动去重 → {1, 2, 3}
s.add(4) # 加一个 → {1, 2, 3, 4}
s.add(2) # 重复加无效,不报错
s.remove(2) # 删掉 2 → {1, 3, 4}
# s.remove(99) ❌ KeyError:删不存在的会崩
s.discard(99) # ✅ discard 删不存在的也不报错
集合运算 ⭐(用符号不用单词,考试填空最爱考):
a = {1, 2, 3}; b = {2, 3, 4}
print(a & b) # 交集 → {2, 3}
print(a | b) # 并集 → {1, 2, 3, 4}
print(a - b) # 差集 → {1}(在 a 不在 b)
print(a ^ b) # 对称差 → {1, 4}(两集合不重叠的部分)
去重最快方式 ⭐:list(set(nums))——先变集合自动去重,再变回列表。
集合推导式:
{x for x in range(10) if x % 2 == 1} # {1, 3, 5, 7, 9}
⚠️ 常见错误(集合)
{}是空字典,要空集合必须写set()。remove删不存在的元素会KeyError;不确定在不在时用discard。- 集合无序:
print({3,1,2})可能输出{1,2,3},别依赖顺序;需要有序结果就sorted()。 - 集合元素也必须可哈希(和字典键同一规矩)。
4.6 深浅拷贝 ⭐(高区分度考点)
先分清「别名」和「复制」——这是新手最容易翻车的点:
a = [1, 2, 3]
b = a # 别名!b 和 a 指向同一份数据
b.append(4)
print(a) # [1, 2, 3, 4] ← a 也跟着变了!因为它们本来就是一份数据
c = a[:] # 切片 = 浅拷贝,新建一份独立数据
d = a.copy() # 也是浅拷贝
c.append(5)
print(a) # [1, 2, 3, 4] ← a 不受影响
但浅拷贝只复制「外壳」。如果列表里装着子列表,外壳是新的,里面的子列表还是共享的:
import copy
matrix = [[0] * 3] * 3 # ⚠️ 三个 [0,0,0] 其实是同一个!
matrix[0][0] = 99
print(matrix) # [[99,0,0],[99,0,0],[99,0,0]] 全变了!
matrix = [[0] * 3 for _ in range(3)] # ✅ 三个独立列表
matrix[0][0] = 99
print(matrix) # [[99,0,0],[0,0,0],[0,0,0]]
m2 = copy.deepcopy(matrix) # 深拷贝:连嵌套的子列表一起复制
一句话记:切片和 copy() 只复制「外壳」;外壳里装的子列表还是共享的。要彻底分家,用 copy.deepcopy()。
⚠️ 常见错误(拷贝)
b = a只是别名,改b等于改a——你以为在复制,其实在共享。[[0]*3]*3:*复制的是引用,三个内层列表是同一个对象。- 深拷贝要
import copy再用copy.deepcopy(...),只在有嵌套结构时才需要。
4.7 成员判断与内置函数
in 对四种结构都适用,语义稍有不同:
3 in [1, 2, 3] # True,列表/元组/集合:判断成员
"小明" in {"小明": 1} # True,字典:判断的是键
常用内置函数:
nums = [3, 1, 2]
print(len(nums)) # 3 长度
print(max(nums)) # 3 最大值
print(min(nums)) # 1 最小值
print(sum(nums)) # 6 求和(数值列表)
print(sorted(nums)) # [1, 2, 3] 返回新列表,不原地改
print(list("abc")) # ['a', 'b', 'c'] 字符串拆成字符列表
print(list(range(5))) # [0, 1, 2, 3, 4] range 转列表
4.8 嵌套数据结构 ⭐
现实中数据是分层的——「学生」里面有「成绩」,「成绩」又分科目。Python 允许任何结构套任何结构,这是综合题的温床。
列表套字典(最常用,模拟「对象数组」):
students = [
{"name": "小明", "score": 90},
{"name": "小红", "score": 95},
]
print(students[1]["score"]) # 先按索引取第 2 个学生,再按键取分数 → 95
字典套列表:
grades = {"数学": [90, 95], "英语": [85, 88]}
print(sum(grades["数学"])) # 185,对列表求和
遍历嵌套结构(一层层剥开):
total = 0
for stu in students: # 先遍历每个学生字典
total += stu["score"] # 再取其中的 score
print(total) # 185
典型综合题套路:学生成绩、购物车结算、分组统计——本质都是「一层层剥开,分别处理」。剥壳顺序:先拿到外层结构 → 遍历 → 取内层字段 → 累加/比较/筛选。
4.9 排序进阶 ⭐
sorted 和 sort 都能传 key,告诉 Python「按什么排序」。这是本讲最后一个重点:
words = ["banana", "fig", "apple"]
print(sorted(words, key=len)) # 按长度 → ['fig', 'apple', 'banana']
print(sorted(words, key=lambda w: w[-1])) # 按最后一个字母 → ['banana', 'apple', 'fig']
多条件排序——用元组当 key,先比第一个条件,再比第二个:
# 按分数从高到低,分数相同按学号从小到大
students = [("A", 90, 2), ("B", 90, 1), ("C", 85, 3)]
ordered = sorted(students, key=lambda s: (-s[1], s[2]))
print(ordered) # [('B', 90, 1), ('A', 90, 2), ('C', 85, 3)]
注意:-s[1] 的负号把升序变成降序(分数高的排前面);学号保持正序。
记忆:key 告诉 Python「比什么」,lambda 写小逻辑,元组实现多条件。
💡 记忆口诀
- 列表书架改删插,元组信封不可动;字典查表靠键快,集合去重关系棒。
- 切片左闭右开:
a[1:3]拿 1、2,不拿 3。 sort原地改,sorted返回新。get保命,setdefault建组,discard不怕删。b = a是别名,a[:]才复制;*3复制的是引用。
📌 双语术语表(本讲)
| 中文 | English |
|---|---|
| 列表 | list |
| 元组 | tuple |
| 字典 | dictionary |
| 集合 | set |
| 推导式 | comprehension |
| 可变 / 不可变 | mutable / immutable |
| 浅拷贝 / 深拷贝 | shallow copy / deep copy |
| 键值对 | key-value pair |
| 去重 | deduplication |
| 交集 / 并集 / 差集 / 对称差 | intersection / union / difference / symmetric difference |
| 解包 | unpack |
| 可哈希 | hashable |
| 嵌套 | nesting |
| 排序键 | sort key |
⭐ 本讲考点清单
- list / tuple / dict / set 差异对比表(有序 / 可变 / 去重)
- 列表推导式(带 if 条件)、字典推导式、集合推导式
- 字典
get()安全取值、setdefault()分组、items()遍历、in判断键 - 集合去重与 交 / 并 / 差 / 对称差运算
- 浅拷贝 vs 深拷贝;
[[0]*3]*3的坑;b=a是别名不是复制 sort()(原地)vssorted()(返回新);key/lambda/元组多条件排序- 词频统计套路
get(w, 0) + 1;setdefault 分组套路 - 元组解包、星号解包、交换变量;单元素元组陷阱
- 嵌套结构(列表套字典)的逐层遍历
- 切片左闭右开、步长、负索引、
[::-1]反转