大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
列表字典与集合
这一课你将能做到
- 熟练操作 list 的增删改查、排序、切片,并说清 tuple 与 list 的取舍
- 用 dict 完成键值存取、遍历、安全取值(get / in),写出统计词频这类高频小任务
- 用 set 完成去重与交并差运算,并根据「要不要有序、要不要去重」四象限选对容器
为什么要学
真实程序 80% 的时间在和数据结构打交道:爬回来的结果存列表、用户配置存字典、标签去重用集合、不该被改的坐标用元组。Java 里你得选 ArrayList、HashMap、HashSet;Python 里对应 list、dict、set,而且是内置类型,直接写 [] {} 就有。容器用不对,后面写文件、写类、做数据分析全是别扭的。
人话:list 是排队(有顺序、可插队);dict 是通讯录(名字查电话,速度快);set 是点名册(只记来没来过,不重复);tuple 是刻在石头上的名单(写完不许改)。
先建立图像
list ["a","b","c"] 有序 · 可变 · 可重复 → 排队
tuple ("a","b","c") 有序 · 不可变 · 可重复 → 石碑
dict {"a":1,"b":2} 键值对 · 键唯一 · 3.7+保持插入序 → 通讯录
set {"a","b"} 无序 · 可变 · 自动去重 → 点名册
四象限选型:
有序 无序
可变 list set
不可变 tuple ——(frozenset 冷门)
需要「键找值」→ dict
跟着做
步骤 1:list 增删改查全流程
新建 collections_demo.py:
nums = [3, 1, 4, 1, 5]
# 查:下标从 0 开始,负数从尾数
print(nums[0], nums[-1]) # 3 5
# 增:append 尾加,insert 指定位置,extend 合并
nums.append(9)
nums.insert(0, 100)
nums.extend([2, 6])
print(nums) # [100, 3, 1, 4, 1, 5, 9, 2, 6]
# 删:remove 按值删第一个,pop 按下标删并返回,del 按下标删
nums.remove(100)
last = nums.pop() # 删尾并返回 6
del nums[0]
print(nums, "被 pop 的:", last) # [3, 1, 4, 1, 5, 9, 2]
# 改:下标直接赋值
nums[0] = 33
print(nums[0]) # 33
# 成员判断与长度
print(5 in nums, len(nums)) # True 7
输出:
3 5
[100, 3, 1, 4, 1, 5, 9, 2, 6]
[3, 1, 4, 1, 5, 9, 2] 被 pop 的: 6
33
True 7
步骤 2:排序与切片
scores = [88, 72, 95, 60]
print(sorted(scores)) # [60, 72, 88, 95] 返回新列表
print(scores) # [88, 72, 95, 60] 原列表没动
scores.sort(reverse=True) # 原地降序,返回 None!
print(scores) # [95, 88, 72, 60]
words = ["banana", "fig", "apple"]
print(sorted(words, key=len)) # 按长度排:['fig', 'apple', 'banana']
# 切片 [start:stop:step],含头不含尾
nums = [0, 1, 2, 3, 4, 5]
print(nums[1:4]) # [1, 2, 3]
print(nums[:3]) # [0, 1, 2] 前 3 个
print(nums[-2:]) # [4, 5] 最后 2 个
print(nums[::2]) # [0, 2, 4] 隔一个取一个
print(nums[::-1]) # [5, 4, 3, 2, 1, 0] 反转
copy = nums[:] # 浅拷贝整个列表
输出:
[60, 72, 88, 95]
[88, 72, 95, 60]
[95, 88, 72, 60]
['fig', 'apple', 'banana']
[1, 2, 3]
[0, 1, 2]
[4, 5]
[0, 2, 4]
[5, 4, 3, 2, 1, 0]
重点区分:sorted() 返回新列表,.sort() 原地修改并返回 None——把 x = lst.sort() 当返回值接是经典 bug。
步骤 3:tuple——不可变序列
point = (3, 5)
x, y = point # 解包
print(x, y) # 3 5
# point[0] = 99 # TypeError!元组不可修改
single = (42,) # 单元素元组必须带逗号
not_a_tuple = (42) # 这只是 int 42
print(type(single), type(not_a_tuple))
# <class 'tuple'> <class 'int'>
输出:
3 5
<class 'tuple'> <class 'int'>
什么时候用 tuple:函数返回多个值、字典的键、坐标/RGB 这类「结构固定的一组值」。规则一句话:会改用 list,不改用 tuple。
步骤 4:dict——键值对核心操作
user = {"name": "小明", "age": 18}
# 取值:[] 键不存在会炸,get 给默认值不炸
print(user["name"]) # 小明
print(user.get("email")) # None
print(user.get("email", "未填写")) # 未填写
# 增/改:同一个方括号
user["email"] = "xm@test.com" # 新键 = 增
user["age"] = 19 # 旧键 = 改
print(user)
# 删
age = user.pop("age") # 删除并返回值
print(age, user) # 19 {'name': '小明', 'email': 'xm@test.com'}
# 判断键是否存在用 in(判断的是键不是值)
print("name" in user) # True
# 三种遍历
for k in user: # 默认遍历键
print(k)
for k, v in user.items(): # 键值对一起拿(最常用)
print(k, "=", v)
输出:
小明
None
未填写
{'name': '小明', 'age': 19, 'email': 'xm@test.com'}
19 {'name': '小明', 'email': 'xm@test.com'}
True
name
email
name = 小明
email = xm@test.com
实战——一行统计词频:
text = "a b a c a b"
freq = {}
for word in text.split():
freq[word] = freq.get(word, 0) + 1
print(freq) # {'a': 3, 'b': 2, 'c': 1}
输出:
{'a': 3, 'b': 2, 'c': 1}
步骤 5:set——去重与集合运算
tags = ["python", "java", "python", "go", "java"]
unique = set(tags)
print(unique) # {'go', 'python', 'java'}(顺序不保证)
print(len(unique)) # 3
# 增删
unique.add("rust")
unique.discard("go") # 不存在也不报错(remove 会报 KeyError)
print(unique)
# 交并差
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a & b) # {3, 4} 交集
print(a | b) # {1, 2, 3, 4, 5, 6} 并集
print(a - b) # {1, 2} 差集:a 有 b 没有
print(a ^ b) # {1, 2, 5, 6} 对称差:只属于一边的
输出:
{'go', 'python', 'java'}
3
{'rust', 'python', 'java'}
{3, 4}
{1, 2, 3, 4, 5, 6}
{1, 2}
{1, 2, 5, 6}
注意:空集合必须写 set()——{} 是空字典。
步骤 6:嵌套结构——真实数据长这样
students = [
{"name": "小明", "scores": [88, 92]},
{"name": "小红", "scores": [75, 81]},
]
for s in students:
avg = sum(s["scores"]) / len(s["scores"])
print(f"{s['name']} 平均分 {avg:.1f}")
输出:
小明 平均分 90.0
小红 平均分 78.0
JSON 解析出来的数据就是这种「字典套列表、列表套字典」的形态,练熟嵌套取值就等于提前练了 JSON。
讲清楚要点
- 可变性是容器的分水岭:list/dict/set 可变(原地修改影响所有引用);tuple/str 不可变(只能整体替换)。第 5 课「函数内改列表外面也变」的现象根源在此。
- dict 的键必须是不可变对象:str、int、tuple 可以当键,list 不行(
TypeError: unhashable type: 'list')。因为 dict 靠 hash 定位键。 - dict 取值两派:
d[k]键不存在抛 KeyError;d.get(k, 默认值)温和返回默认值。读配置、统计场景用 get,确认键一定存在用 [](错了就该炸出来提醒你)。 - 含头不含尾贯穿始终:切片
[1:4]、range(1, 4) 都取 1、2、3。记住这条,切片和 range 的 bug 少一半。 in对四类容器都适用:list/tuple 慢(逐个比),dict/set 快(hash 直达)。大数据量的成员判断一律换成 set。- set 无序且元素必须不可变:不能
s[0]取元素(没有下标),可变对象进不了 set。去重会丢掉原有顺序,要保序去重用dict.fromkeys(lst)。 - 浅拷贝陷阱:
b = a[:]只拷贝外层;嵌套列表里的子列表仍共享。真正独立的拷贝用copy.deepcopy(a)。
你可能会懵的地方
-
dict 到底有没有序? Python 3.7 起官方保证 dict 记住插入顺序,遍历按插入先后。但它不是排序——按值排序要自己
sorted(d.items(), key=...)。更早版本无序,老资料别看混。 -
为什么
d["a"] += 1有时报 KeyError?+=等价于先取后写:d["a"] = d["a"] + 1。键不存在时「取」这一步就炸了。首次累加用d[k] = d.get(k, 0) + 1,或collections.defaultdict(int)。 -
tuple 既然不可变,里面装个 list 呢? 元组本身不可变指的是「槽位不能换」,槽位里的可变对象内容仍能改:
t = (1, [2, 3])后t[1].append(4)合法。这也是 tuple 不能当 dict 键的原因之一(可 hash 要求内容整体不可变)。 -
b = a和b = a[:]差在哪?b = a两个名字贴同一个列表,改一个「两个都变」;b = a[:]是浅拷贝,外层独立、内层嵌套对象仍共享。想完全独立用copy.deepcopy。 -
什么时候用列表推导式,什么时候用循环? 简单转换/过滤用推导式(一行读完);逻辑超过两层嵌套、或者边遍历边有副作用(写文件、print 日志),用普通循环。推导式是优化可读性的,不是炫技的。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
x = lst.sort() | sort 返回 None,x 变 None | lst.sort() 或 x = sorted(lst) |
d[key] 直接取可能不存在的键 | KeyError 中断程序 | d.get(key, 默认值) 或先 in 判断 |
{} 当空集合用 | {} 是空字典 | 空集合写 set() |
| 用 list 当 dict 的键 | TypeError: unhashable | 换 tuple 或字符串 |
for x in lst 时 lst.remove(x) | 边遍历边删,跳元素 | 遍历副本 for x in lst[:] 或列表推导式重建 |
b = a 以为是复制 | 两个名字共享同一对象 | b = a[:](浅)或 copy.deepcopy(深) |
单元素元组写 (42) | 得到 int 42 | (42,) 必须带逗号 |
卡点急救
- KeyError:dict 键不存在。用 get 带默认值,或
if k in d先判断;统计累加场景用d.get(k, 0) + 1。 - TypeError: unhashable type: 'list':把 list 用成了 dict 键或放进了 set。把内层改成 tuple:
tuple(mylist)。 - IndexError: list index out of range:下标越界。先
len(lst)看长度;负下标最小到-len。 - 改了 a 结果 b 也变了:引用共享问题。需要独立副本用切片或 deepcopy。
- remove 报 ValueError:要删的值不在列表里。先
if v in lst:再删,或用 discard 语义的 try/except(第 11 课)。 - 遍历中增删列表行为诡异:Python 遍历按下标推进,删除会跳元素。改为遍历副本或用推导式生成新列表。
面试常问
-
list 和 tuple 的区别,各自使用场景? list 可变、tuple 不可变。tuple 用于结构固定的数据(坐标、函数多返回值)、作为 dict 键、以及向调用方表达「这份数据不该被改」的语义。不可变还让 tuple 可 hash、更省内存。
-
dict 的底层实现和查找复杂度? 哈希表。键经 hash 定位桶,平均 O(1) 查找/插入,最坏 O(n)(哈希冲突严重时)。这也是「键必须可 hash(不可变)」的原因。Python 3.7+ dict 保序且做了内存紧凑化。
-
set 和 dict 的关系? set 本质是「只有键没有值」的 dict,同样基于哈希表,成员判断平均 O(1)。所以 set 元素也必须不可变、无序、自动去重。
-
如何对列表去重且保持顺序?
list(dict.fromkeys(lst))(3.7+ dict 有序);或遍历 + seen 集合判断。直接list(set(lst))会丢失原顺序。 -
浅拷贝与深拷贝的区别? 浅拷贝只复制容器本身,内部嵌套对象仍共享引用(
lst[:]、list(lst)、copy.copy);深拷贝递归复制所有层级(copy.deepcopy),完全独立。嵌套结构需要真正隔离时必须深拷贝。
自测三问
sorted(lst)和lst.sort()的三个区别(返回值、原列表、调用方式)?- 如何安全地给字典里可能不存在的键累加 1?写出代码。
[1, 2, 2, 3, 3, 3]去重并保持原顺序,写出代码。
小结一句话
list 排队、dict 通讯录、set 点名册、tuple 石碑;记住 sort 返回 None、get 不炸、含头不含尾、赋值是共享不是复制,容器这一关就通了。
延伸
- 官方教程 — 数据结构
- collections 模块(defaultdict、Counter 等)
- 下一课预告:字符串处理——切片、f-string、split/join,文本是脚本世界处理量最大的数据。