Skip to content

Python dict 和 set:从"通讯录"到"俱乐部会员"的数据管理艺术

引言:为什么需要"查找"结构?

想象你是一本大型通讯录的管理员,里面记录了 10 万个联系人的姓名和电话。如果有人问:"张三的电话是多少?",你会怎么找?

  • 笨办法:从第一页开始,一页一页翻,直到找到"张三"为止。如果张三在最后一页,你要翻 10 万次。
  • 聪明办法:通讯录按拼音排序,你先查"Z"开头的部分,再快速定位到"Zhang",最后找到"张三"。最多翻几十页就能找到。

在编程世界里,dict 就是这本"智能通讯录",它能在几乎一瞬间从 10 万条数据中找到你要的那一条。而 set 则像一个俱乐部会员名单,只记录"谁在俱乐部里",不记录额外信息。


一、dict:你的"智能通讯录"

1.1 什么是 dict?

dict(字典)是 Python 内置的**键-值对(key-value)**存储结构。用花括号 {} 表示,每个元素是 key: value 的形式。

python
# 学生成绩表
scores = {'Michael': 95, 'Bob': 75, 'Tracy': 85}

# 用户信息
user = {'name': '张三', 'age': 25, 'city': '北京'}

# 商品库存
inventory = {'苹果': 100, '香蕉': 50, '橙子': 80}

生活化理解:dict 就像你手机里的通讯录——姓名(key)对应电话号码(value),输入姓名立刻找到电话。

1.2 为什么 dict 查找这么快?

list 的查找方式(线性查找):

python
names = ['Michael', 'Bob', 'Tracy']    # 1万个人名
scores = [95, 75, 85]                  # 对应的分数

# 找 'Tracy' 的成绩:先遍历 names 找到位置,再取 scores
# 最坏情况要遍历 1 万次

dict 的查找方式(哈希查找):

python
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}

# 找 'Tracy' 的成绩:直接计算 'Tracy' 的存储位置,一步到位
# 无论数据量多大,速度几乎不变

生活化理解

  • list 像没有索引的字典,查字要从第一页翻到最后一页
  • dict 像有部首索引的字典,先查部首表,直接翻到对应页码

dict 内部使用哈希算法(Hash),根据 key 直接计算出 value 的内存地址,所以查找速度极快。

1.3 创建 dict 的几种方式

python
# 方式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 访问和修改元素

python
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,重复赋值会覆盖:

python
d['Jack'] = 90
d['Jack'] = 88    # 覆盖前面的 90
print(d['Jack'])  # 88

1.5 避免 KeyError 的三种方法

如果访问不存在的 key,dict 会报错:

python
d = {'Michael': 95}
print(d['Thomas'])    # KeyError: 'Thomas'

方法 1:in 判断

python
if 'Thomas' in d:
    print(d['Thomas'])
else:
    print('不存在')

方法 2:get() 方法

python
print(d.get('Thomas'))        # 返回 None(不报错)
print(d.get('Thomas', -1))    # 返回默认值 -1

方法 3:setdefault()

python
# 如果 key 存在,返回对应值;不存在,设置默认值并返回
value = d.setdefault('Thomas', 60)
print(value)    # 60
print(d)        # {'Michael': 95, 'Thomas': 60}

1.6 删除元素

python
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 的常用操作

操作方法示例
获取所有 keykeys()d.keys()
获取所有 valuevalues()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 的遍历

python
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 最重要的规则:

python
# ✅ 可以:字符串、整数、元组(不可变)
d = {'name': '张三', 25: 'age', (1, 2): 'point'}

# ❌ 报错:列表、字典、集合(可变)
d = {[1, 2]: 'list'}    # TypeError: unhashable type: 'list'

为什么? 因为 dict 要根据 key 计算存储位置(哈希值)。如果 key 可变,今天计算的位置和明天不同,dict 就乱了。

生活化理解:通讯录按姓名排序,如果姓名可以随便改,那排序就失效了。

1.10 dict vs list:空间换时间

对比维度dictlist
查找速度极快(O(1))慢(O(n))
内存占用
有序性无序(Python 3.7+ 保持插入顺序)有序
适用场景快速查找顺序存储

结论:dict 用更多的内存换取了更快的查找速度。


二、set:你的"俱乐部会员名单"

2.1 什么是 set?

set(集合)是无序、不重复的元素集合。用花括号 {} 表示,但没有 key-value 对。

python
# 创建 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 的核心特性:自动去重

python
s = {1, 1, 2, 2, 3, 3}
print(s)    # {1, 2, 3}

应用场景:快速去除 list 中的重复元素

python
l = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique = list(set(l))
print(unique)    # [1, 2, 3, 4]

2.3 set 的添加和删除

python
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 支持数学意义上的集合运算:

python
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 的元素必须是不可变对象:

python
# ✅ 可以
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 不可变对象的"假修改"

python
# 字符串是不可变的
a = 'abc'
b = a.replace('a', 'A')

print(a)    # 'abc'(原字符串没变)
print(b)    # 'Abc'(新字符串)

生活化理解

  • 字符串像刻在石头上的字——不能改,只能换一块新石头
  • replace() 不是"修改"原字符串,而是创建了一个新字符串

3.3 变量 vs 对象:指向关系

python
a = 'abc'

这里 'abc'对象(实际存在的字符串),a变量(指向对象的标签)。

python
┌───┐     ┌────────┐
│ a │────▶│ 'abc'
└───┘     └────────┘

调用 a.replace('a', 'A') 时:

  1. 创建新对象 'Abc'
  2. 返回新对象
  3. 变量 a 仍然指向 'abc'
python
b = a.replace('a', 'A')

┌───┐     ┌────────┐
│ a │────▶│ 'abc'
└───┘     └────────┘

┌───┐     ┌────────┐
│ b │────▶│ 'Abc'
└───┘     └────────┘

3.4 可变对象的"真修改"

python
a = ['c', 'b', 'a']
a.sort()    # 原地修改,返回 None

print(a)    # ['a', 'b', 'c']

list 的 sort() 方法直接在原对象上修改,不创建新对象。


四、常见误区与陷阱

4.1 误区一:dict 的 key 可以是任何类型

python
# ❌ 错误: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
# 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 有顺序

python
# ❌ 错误理解:set 有顺序
s = {3, 1, 2}
print(s)    # {1, 2, 3}(看起来像有序,实际是哈希结果)

# ✅ 正确理解:set 无序,不要依赖顺序
# 需要排序先转 list
sorted_list = sorted(s)

4.4 误区四:get() 和 setdefault() 混淆

python
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 误区五:可变对象作为默认参数

python
# ❌ 危险:默认参数只创建一次
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:词频统计

python
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:分组数据

python
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:好友关系分析

python
# 两个人的共同好友
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:缓存系统

python
# 用 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 查找 valuedict键值对设计,查找极快
只需要判断元素是否存在set自动去重,查找快
需要保持插入顺序dict (3.7+)dict 保持插入顺序
需要去重set自动去重
需要数学集合运算set支持交并差等运算
需要存储额外信息dictvalue 可以存任何数据
需要统计次数dictvalue 作为计数器

七、动手练习

练习 1:把 (1, 2, 3)(1, [2, 3]) 分别放入 dict 和 set,观察结果。

python
# 测试 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 listdict 空间换时间,list 时间换空间
set 运算交集 &、并集 |、差集 -、对称差集 ^

记住:dict 是你的智能通讯录,set 是你的俱乐部会员名单。需要键值对用 dict,只需要去重用 set。两者的 key/元素都必须是不可变对象