04 · 组合数据类型(Data Structures)

📅 预计 100 分钟 | ⭐ = 最高频考点章
✍️ 本讲配套练习:本地 python grader.py 04,或网页练习


4.0 为什么要学组合数据类型

前几章里,一个变量只能存一个值:x = 5name = "小明"。但现实中的数据从来不是孤零零一个——你有一整架书、一张全班成绩表、一堆要找出重复的编号。组合数据类型(也叫数据结构)就是 Python 提供的一批「一次装下很多东西」的容器,本讲四兄弟:列表、元组、字典、集合。

先记住四个生活比喻,后面所有知识点都挂在这四个钩子上,忘不掉:

结构 生活比喻 一句话概括
列表 list 书架 一格一格按顺序摆,可加、可拿、可换
元组 tuple 只读信封 封好的信,不能拆改,只能整体传阅
字典 dict 查表 名字 → 电话,查号台,按 key 秒定位
集合 set 去重袋 丢进去自动去重,还能做交并差

4.1 四种结构总览 ⭐

类型 符号 有序 可变 允许重复 场景
列表 list [1,2] 最常用,默认选项
元组 tuple (1,2) 只读数据、字典键
字典 dict {"a":1} ✅(3.7+) 键不重复 键值映射、快速查找
集合 set {1,2} ❌(自动去重) 去重、关系运算

怎么记?三句话:

  1. 列表是默认选项——九成场景先想它。
  2. 要只读、要当字典键 → 选元组
  3. 查得快用字典,去重/求关系用集合

⚠️ 常见错误:把四种结构混为一谈

  • {} 是空字典,不是空集合;空集合必须写 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] 带条件的推导式(高频)

⚠️ 常见错误(列表)

  1. 索引越界:只有 3 个元素却写 nums[5],直接 IndexError 崩溃;但切片越界不报错,只给你能取到的部分。
  2. sort()sorted() 搞混sort() 原地改、返回 Nonesorted() 返回新列表。写 nums = nums.sort() 会把列表变成 None,经典大坑。
  3. 复制还是别名b = a 不是复制,是给同一份数据起第二个名字(详见 4.6)。
  4. remove 删的是第一个匹配的值,且值不存在会 ValueErrorpop(空列表)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)。

⚠️ 常见错误(元组)

  1. 忘了单元素加逗号t = (5) 结果是整数,不是元组。
  2. 解包数量不匹配a, b = (1, 2, 3)ValueError: too many values to unpack
  3. 以为元组能像列表一样 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 再存回去。

⚠️ 常见错误(字典)

  1. KeyError:直接 d[键] 取值而键不存在就会崩;不确定时一律用 d.get(键, 默认值)
  2. 键必须可哈希:数字、字符串、元组可以当键;列表、字典不能当键TypeError: unhashable type)。原因:列表可变,哈希值会变,Python 就找不到它了。
  3. in 判断的是键不是值"138" in phoneFalse
  4. 遍历时别删键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}

⚠️ 常见错误(集合)

  1. {} 是空字典,要空集合必须写 set()
  2. remove 删不存在的元素会 KeyError;不确定在不在时用 discard
  3. 集合无序print({3,1,2}) 可能输出 {1,2,3},别依赖顺序;需要有序结果就 sorted()
  4. 集合元素也必须可哈希(和字典键同一规矩)。

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()

⚠️ 常见错误(拷贝)

  1. b = a 只是别名,改 b 等于改 a——你以为在复制,其实在共享。
  2. [[0]*3]*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 排序进阶 ⭐

sortedsort 都能传 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

⭐ 本讲考点清单

  1. list / tuple / dict / set 差异对比表(有序 / 可变 / 去重)
  2. 列表推导式(带 if 条件)、字典推导式、集合推导式
  3. 字典 get() 安全取值、setdefault() 分组、items() 遍历、in 判断键
  4. 集合去重与 交 / 并 / 差 / 对称差运算
  5. 浅拷贝 vs 深拷贝;[[0]*3]*3 的坑;b=a 是别名不是复制
  6. sort()(原地)vs sorted()(返回新);key/lambda/元组多条件排序
  7. 词频统计套路 get(w, 0) + 1;setdefault 分组套路
  8. 元组解包、星号解包、交换变量;单元素元组陷阱
  9. 嵌套结构(列表套字典)的逐层遍历
  10. 切片左闭右开、步长、负索引、[::-1] 反转