Python dict 和 set:从"通讯录"到"俱乐部会员"的数据管理艺术
引言:为什么需要"查找"结构?
想象你是一本大型通讯录的管理员,里面记录了 10 万个联系人的姓名和电话。如果有人问:"张三的电话是多少?",你会怎么找?
- 笨办法:从第一页开始,一页一页翻,直到找到"张三"为止。如果张三在最后一页,你要翻 10 万次。
- 聪明办法:通讯录按拼音排序,你先查"Z"开头的部分,再快速定位到"Zhang",最后找到"张三"。最多翻几十页就能找到。
在编程世界里,dict 就是这本"智能通讯录",它能在几乎一瞬间从 10 万条数据中找到你要的那一条。而 set 则像一个俱乐部会员名单,只记录"谁在俱乐部里",不记录额外信息。
一、dict:你的"智能通讯录"
1.1 什么是 dict?
dict(字典)是 Python 内置的**键-值对(key-value)**存储结构。用花括号 {} 表示,每个元素是 key: value 的形式。
# 学生成绩表
scores = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
# 用户信息
user = {'name': '张三', 'age': 25, 'city': '北京'}
# 商品库存
inventory = {'苹果': 100, '香蕉': 50, '橙子': 80}生活化理解:dict 就像你手机里的通讯录——姓名(key)对应电话号码(value),输入姓名立刻找到电话。
1.2 为什么 dict 查找这么快?
list 的查找方式(线性查找):
names = ['Michael', 'Bob', 'Tracy'] # 1万个人名
scores = [95, 75, 85] # 对应的分数
# 找 'Tracy' 的成绩:先遍历 names 找到位置,再取 scores
# 最坏情况要遍历 1 万次dict 的查找方式(哈希查找):
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
# 找 'Tracy' 的成绩:直接计算 'Tracy' 的存储位置,一步到位
# 无论数据量多大,速度几乎不变生活化理解:
- list 像没有索引的字典,查字要从第一页翻到最后一页
- dict 像有部首索引的字典,先查部首表,直接翻到对应页码
dict 内部使用哈希算法(Hash),根据 key 直接计算出 value 的内存地址,所以查找速度极快。
1.3 创建 dict 的几种方式
# 方式1:直接定义
d1 = {'a': 1, 'b': 2, 'c': 3}
# 方式2:空 dict,后续添加
d2 = {}
d2['name'] = '张三'
# 方式3:用 dict() 函数
d3 = dict(a=1, b=2, c=3)
# 方式4:从列表转换
d4 = dict([('a', 1), ('b', 2), ('c', 3)])1.4 访问和修改元素
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
# 访问:直接用 key
print(d['Michael']) # 95
# 修改:直接赋值
d['Michael'] = 98
print(d['Michael']) # 98
# 添加新元素:同样直接赋值
d['Adam'] = 67
print(d) # {'Michael': 98, 'Bob': 75, 'Tracy': 85, 'Adam': 67}注意:同一个 key 只能对应一个 value,重复赋值会覆盖:
d['Jack'] = 90
d['Jack'] = 88 # 覆盖前面的 90
print(d['Jack']) # 881.5 避免 KeyError 的三种方法
如果访问不存在的 key,dict 会报错:
d = {'Michael': 95}
print(d['Thomas']) # KeyError: 'Thomas'方法 1:in 判断
if 'Thomas' in d:
print(d['Thomas'])
else:
print('不存在')方法 2:get() 方法
print(d.get('Thomas')) # 返回 None(不报错)
print(d.get('Thomas', -1)) # 返回默认值 -1方法 3:setdefault()
# 如果 key 存在,返回对应值;不存在,设置默认值并返回
value = d.setdefault('Thomas', 60)
print(value) # 60
print(d) # {'Michael': 95, 'Thomas': 60}1.6 删除元素
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
# pop():删除并返回 value
score = d.pop('Bob')
print(score) # 75
print(d) # {'Michael': 95, 'Tracy': 85}
# del 语句:直接删除
del d['Tracy']
print(d) # {'Michael': 95}
# clear():清空所有元素
d.clear()
print(d) # {}1.7 dict 的常用操作
| 操作 | 方法 | 示例 |
|---|---|---|
| 获取所有 key | keys() | d.keys() |
| 获取所有 value | values() | d.values() |
| 获取所有键值对 | items() | d.items() |
| 检查 key 是否存在 | in | 'a' in d |
| 获取 value(带默认值) | get(key, default) | d.get('a', 0) |
| 删除并返回 | pop(key) | d.pop('a') |
| 清空 | clear() | d.clear() |
| 更新(合并) | update() | d.update(d2) |
1.8 dict 的遍历
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
# 遍历 key
for key in d:
print(key)
# 遍历 value
for value in d.values():
print(value)
# 遍历 key-value
for key, value in d.items():
print(f'{key}: {value}')1.9 dict 的 key 必须是不可变对象
这是 dict 最重要的规则:
# ✅ 可以:字符串、整数、元组(不可变)
d = {'name': '张三', 25: 'age', (1, 2): 'point'}
# ❌ 报错:列表、字典、集合(可变)
d = {[1, 2]: 'list'} # TypeError: unhashable type: 'list'为什么? 因为 dict 要根据 key 计算存储位置(哈希值)。如果 key 可变,今天计算的位置和明天不同,dict 就乱了。
生活化理解:通讯录按姓名排序,如果姓名可以随便改,那排序就失效了。
1.10 dict vs list:空间换时间
| 对比维度 | dict | list |
|---|---|---|
| 查找速度 | 极快(O(1)) | 慢(O(n)) |
| 内存占用 | 大 | 小 |
| 有序性 | 无序(Python 3.7+ 保持插入顺序) | 有序 |
| 适用场景 | 快速查找 | 顺序存储 |
结论:dict 用更多的内存换取了更快的查找速度。
二、set:你的"俱乐部会员名单"
2.1 什么是 set?
set(集合)是无序、不重复的元素集合。用花括号 {} 表示,但没有 key-value 对。
# 创建 set
s = {1, 2, 3}
# 从 list 创建(自动去重)
s = set([1, 2, 2, 3, 3, 3])
print(s) # {1, 2, 3}
# 空 set(注意:{} 是空 dict)
s = set()生活化理解:set 就像俱乐部会员名单——只记录"谁在",不记录"是谁的什么",且同一个人不能重复登记。
2.2 set 的核心特性:自动去重
s = {1, 1, 2, 2, 3, 3}
print(s) # {1, 2, 3}应用场景:快速去除 list 中的重复元素
l = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique = list(set(l))
print(unique) # [1, 2, 3, 4]2.3 set 的添加和删除
s = {1, 2, 3}
# 添加
s.add(4)
print(s) # {1, 2, 3, 4}
# 重复添加无效果
s.add(4)
print(s) # {1, 2, 3, 4}
# 删除
s.remove(4)
print(s) # {1, 2, 3}
# 删除不存在的元素会报错
# s.remove(5) # KeyError: 5
# discard():删除不存在的元素不报错
s.discard(5) # 无操作2.4 set 的数学运算
set 支持数学意义上的集合运算:
s1 = {1, 2, 3}
s2 = {2, 3, 4}
# 交集:两个集合都有的元素
print(s1 & s2) # {2, 3}
# 并集:两个集合的所有元素(去重)
print(s1 | s2) # {1, 2, 3, 4}
# 差集:在 s1 中但不在 s2 中
print(s1 - s2) # {1}
# 对称差集:只在其中一个集合中
print(s1 ^ s2) # {1, 4}生活化理解:
- 交集:两个俱乐部的共同会员
- 并集:两个俱乐部的所有会员(去重)
- 差集:只参加了俱乐部 A,没参加俱乐部 B 的人
2.5 set 的常用操作
| 操作 | 方法 | 示例 |
|---|---|---|
| 添加 | add(x) | s.add(4) |
| 删除 | remove(x) | s.remove(4) |
| 安全删除 | discard(x) | s.discard(4) |
| 交集 | & 或 intersection() | s1 & s2 |
| 并集 | | 或 union() | s1 | s2 |
| 差集 | - 或 difference() | s1 - s2 |
| 对称差集 | ^ 或 symmetric_difference() | s1 ^ s2 |
| 子集判断 | issubset() | s1.issubset(s2) |
| 超集判断 | issuperset() | s1.issuperset(s2) |
2.6 set 的元素也必须不可变
与 dict 的 key 一样,set 的元素必须是不可变对象:
# ✅ 可以
s = {1, 'a', (1, 2)}
# ❌ 报错
s = {[1, 2]} # TypeError: unhashable type: 'list'三、不可变对象:理解 Python 的核心概念
3.1 什么是不可变对象?
不可变对象(immutable):创建后不能被修改的对象。
Python 中的不可变对象:
- 数字(int, float)
- 字符串(str)
- 元组(tuple)
- 冻结集合(frozenset)
可变对象(mutable):
- 列表(list)
- 字典(dict)
- 集合(set)
3.2 不可变对象的"假修改"
# 字符串是不可变的
a = 'abc'
b = a.replace('a', 'A')
print(a) # 'abc'(原字符串没变)
print(b) # 'Abc'(新字符串)生活化理解:
- 字符串像刻在石头上的字——不能改,只能换一块新石头
replace()不是"修改"原字符串,而是创建了一个新字符串
3.3 变量 vs 对象:指向关系
a = 'abc'这里 'abc' 是对象(实际存在的字符串),a 是变量(指向对象的标签)。
┌───┐ ┌────────┐
│ a │────▶│ 'abc' │
└───┘ └────────┘调用 a.replace('a', 'A') 时:
- 创建新对象
'Abc' - 返回新对象
- 变量
a仍然指向'abc'
b = a.replace('a', 'A')
┌───┐ ┌────────┐
│ a │────▶│ 'abc' │
└───┘ └────────┘
┌───┐ ┌────────┐
│ b │────▶│ 'Abc' │
└───┘ └────────┘3.4 可变对象的"真修改"
a = ['c', 'b', 'a']
a.sort() # 原地修改,返回 None
print(a) # ['a', 'b', 'c']list 的 sort() 方法直接在原对象上修改,不创建新对象。
四、常见误区与陷阱
4.1 误区一:dict 的 key 可以是任何类型
# ❌ 错误:list 是可变的,不能作为 key
d = {[1, 2]: 'value'} # TypeError: unhashable type: 'list'
# ✅ 正确:使用不可变类型
d = {(1, 2): 'value'} # tuple 可以
d = {'key': 'value'} # 字符串可以
d = {123: 'value'} # 整数可以4.2 误区二:dict 的遍历顺序
# Python 3.7 之前:dict 无序
# Python 3.7+:保持插入顺序
d = {'b': 2, 'a': 1, 'c': 3}
print(d) # {'b': 2, 'a': 1, 'c': 3}
# 但最好不要依赖顺序,需要排序用 sorted()
for key in sorted(d):
print(key, d[key])4.3 误区三:set 有顺序
# ❌ 错误理解:set 有顺序
s = {3, 1, 2}
print(s) # {1, 2, 3}(看起来像有序,实际是哈希结果)
# ✅ 正确理解:set 无序,不要依赖顺序
# 需要排序先转 list
sorted_list = sorted(s)4.4 误区四:get() 和 setdefault() 混淆
d = {'a': 1}
# get():只获取,不修改
print(d.get('b', 2)) # 2
print(d) # {'a': 1}(d 没变)
# setdefault():获取或设置
print(d.setdefault('b', 2)) # 2
print(d) # {'a': 1, 'b': 2}(d 被修改了)4.5 误区五:可变对象作为默认参数
# ❌ 危险:默认参数只创建一次
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2](不是预期的 [2]!)
# ✅ 正确:使用 None 作为默认值
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items五、实战案例
案例 1:词频统计
text = "apple banana apple orange banana apple"
words = text.split()
# 统计每个单词出现的次数
word_count = {}
for word in words:
if word in word_count:
word_count[word] += 1
else:
word_count[word] = 1
print(word_count)
# {'apple': 3, 'banana': 2, 'orange': 1}
# 更简洁的方法
from collections import Counter
word_count = Counter(words)
print(word_count.most_common(2)) # [('apple', 3), ('banana', 2)]案例 2:分组数据
students = [
{'name': '张三', 'class': '一班', 'score': 85},
{'name': '李四', 'class': '二班', 'score': 92},
{'name': '王五', 'class': '一班', 'score': 78},
{'name': '赵六', 'class': '二班', 'score': 88}
]
# 按班级分组
class_groups = {}
for student in students:
class_name = student['class']
if class_name not in class_groups:
class_groups[class_name] = []
class_groups[class_name].append(student)
print(class_groups)
# {'一班': [{'name': '张三', ...}, {'name': '王五', ...}],
# '二班': [{'name': '李四', ...}, {'name': '赵六', ...}]}案例 3:好友关系分析
# 两个人的共同好友
alice_friends = {'Bob', 'Charlie', 'David', 'Eve'}
bob_friends = {'Charlie', 'David', 'Frank', 'Grace'}
# 共同好友(交集)
common = alice_friends & bob_friends
print(f'共同好友:{common}') # {'Charlie', 'David'}
# Alice 独有好友(差集)
only_alice = alice_friends - bob_friends
print(f'只有 Alice 认识:{only_alice}') # {'Bob', 'Eve'}
# 所有好友(并集)
all_friends = alice_friends | bob_friends
print(f'所有好友:{all_friends}')案例 4:缓存系统
# 用 dict 实现简单缓存
cache = {}
def expensive_calculation(n):
# 检查缓存
if n in cache:
print(f'从缓存获取:{n}')
return cache[n]
# 模拟耗时计算
print(f'计算中:{n}')
result = n ** 2
# 存入缓存
cache[n] = result
return result
print(expensive_calculation(5)) # 计算中:5 → 25
print(expensive_calculation(5)) # 从缓存获取:5 → 25
print(expensive_calculation(3)) # 计算中:3 → 9六、dict 和 set 的选择指南
| 场景 | 推荐数据结构 | 原因 |
|---|---|---|
| 需要根据 key 查找 value | dict | 键值对设计,查找极快 |
| 只需要判断元素是否存在 | set | 自动去重,查找快 |
| 需要保持插入顺序 | dict (3.7+) | dict 保持插入顺序 |
| 需要去重 | set | 自动去重 |
| 需要数学集合运算 | set | 支持交并差等运算 |
| 需要存储额外信息 | dict | value 可以存任何数据 |
| 需要统计次数 | dict | value 作为计数器 |
七、动手练习
练习 1:把 (1, 2, 3) 和 (1, [2, 3]) 分别放入 dict 和 set,观察结果。
# 测试 tuple 作为 dict 的 key
d = {}
d[(1, 2, 3)] = 'ok' # ✅ 可以
# d[(1, [2, 3])] = 'error' # ❌ 报错:tuple 中有 list,不可哈希
# 测试 tuple 作为 set 的元素
s = set()
s.add((1, 2, 3)) # ✅ 可以
# s.add((1, [2, 3])) # ❌ 报错结论:tuple 本身是不可变的,但如果包含可变元素(如 list),就不可哈希,不能作为 dict 的 key 或 set 的元素。
总结
| 概念 | 核心要点 |
|---|---|
| dict | 键值对存储,查找极快,key 必须不可变 |
| set | 无序不重复集合,元素必须不可变 |
| 哈希算法 | 根据 key 计算存储位置的算法 |
| 不可变对象 | str, int, tuple 等,创建后不能修改 |
| 可变对象 | list, dict, set 等,创建后可修改 |
| dict vs list | dict 空间换时间,list 时间换空间 |
| set 运算 | 交集 &、并集 |、差集 -、对称差集 ^ |
记住:dict 是你的智能通讯录,set 是你的俱乐部会员名单。需要键值对用 dict,只需要去重用 set。两者的 key/元素都必须是不可变对象。