使用dict和set

在Python的内置数据结构里,列表和元组用来存有序序列已经足够顺手,但遇到快速通过「唯一标识」定位数据、一键批量去重、高效成员关系判断这种高频场景,「字典dict」和「集合set」绝对是不可替代的黄金搭档——核心原因是它们底层都用了哈希表(Hash Table) 的“黑科技”!


字典(dict)基础

Python的字典是典型的键值对(key-value)映射容器,其他语言中也叫哈希表(Hash Table)、映射(Map)、关联数组(Associative Array)等。

基本的增删改查

字典的语法非常直观,用花括号包裹key: value对,逗号分隔。

# 创建空字典(注意不能直接写{},会被识别成空集合!)
empty_dict = {}
empty_dict = dict()  # 更推荐这种显式方式

# 创建带初始值的字典
student_scores = {"Michael": 95, "Bob": 75, "Tracy": 85}

# 访问元素:直接用[key](不存在会报错KeyError)
print(student_scores["Michael"])  # 输出: 95

# 添加/修改元素:直接赋值
student_scores["Adam"] = 67  # 新增键值对
student_scores["Bob"] = 80  # 修改已有键对应的值(键唯一,后赋值覆盖前)

# 删除元素
removed_score = student_scores.pop("Bob")  # 安全删除+返回值,不存在同样KeyError
del student_scores["Tracy"]  # 无返回值删除,不存在同样KeyError
student_scores.clear()  # 清空所有元素

高效性的核心:哈希表原理

字典为什么能做到接近O(1)的查询/插入/删除速度?简单来说是靠三步:

  1. 对输入的key执行内置的哈希函数hash(),得到一个固定的“哈希值编码”
  2. 把哈希值编码压缩成容器内部数组的索引位置
  3. 根据索引直接定位到value的存储地址

整个过程不需要遍历所有元素,所以数据量从10涨到10000,速度基本没变化。

常用的进阶操作

# 先补回之前的测试数据
student_scores = {"Michael": 95, "Bob": 80, "Tracy": 85}

# ✅ 安全检查键是否存在(推荐替代try-except+直接索引)
if "Michael" in student_scores:
    print("找到了Michael的成绩")

# ✅ 安全获取值(避免KeyError,第二个参数是默认值)
score = student_scores.get("Thomas", -1)  # 不存在返回-1,默认是None
print(score)  # 输出: -1

# ✅ 获取容器的“视图对象”(动态关联原字典,原数据变视图自动变)
keys = student_scores.keys()    # 所有键的视图
values = student_scores.values()  # 所有值的视图
items = student_scores.items()   # 所有键值对的视图(元组形式)
# 视图可以转成列表,但转列表后就和原字典解耦了
print(list(items))  # 输出: [("Michael",95), ("Bob",80), ("Tracy",85)]

# ✅ 字典推导式(快速生成/过滤字典)
squares = {x: x**2 for x in range(5)}  # {0:0,1:1,2:4,3:9,4:16}
high_scores = {k:v for k,v in student_scores.items() if v > 80}  # {"Tracy":85}

三个不能忘的核心特点

  1. 插入有序(语言规范3.7+),但别完全依赖:Python 3.6+是实现层面有序,3.7+写入了规范,但跨版本或底层优化后可能调整逻辑,业务强依赖顺序请用collections.OrderedDict
  2. 键必须唯一:后赋值的同键会直接覆盖前一个
  3. 键必须是不可变对象:哈希表需要稳定的哈希值,可变对象(列表、字典、普通集合)的哈希值会随内容变,会破坏内部索引结构

集合(set)基础

集合可以理解成只存键、不存值的字典,底层同样靠哈希表实现,天生适合做去重和集合运算。

基本的使用和运算

# 创建空集合(必须用set(),{}是空字典!)
empty_set = set()

# 创建带初始值的集合(自动去重)
num_set = {1, 2, 3, 2}  # 输出后是{1, 2, 3}
num_set = set([3, 2, 1, 2])  # 从列表/元组/字符串转集合(同样去重)

# 添加元素
num_set.add(4)  # 加单个元素
num_set.update([5, 6, 6])  # 加可迭代对象(自动去重添加)

# 删除元素
num_set.remove(3)  # 不存在会报错KeyError
num_set.discard(3)  # 安全删除,不存在不报错
num_set.pop()  # 随机删除一个元素(集合无序)
num_set.clear()  # 清空所有元素

# ✅ 数学集合运算(也有对应的方法,比如s1.intersection(s2))
s1 = {1, 2, 3}
s2 = {2, 3, 4}

print(s1 & s2)  # 交集:两个集合都有的元素 → {2, 3}
print(s1 | s2)  # 并集:所有出现过的元素 → {1, 2, 3, 4}
print(s1 - s2)  # 差集:s1有但s2没有的 → {1}
print(s1 ^ s2)  # 对称差集:只在一个集合出现的 → {1, 4}

集合的三个核心特点

和字典的键几乎一致:

  1. 无序性:同样不要依赖遍历顺序
  2. 唯一性:自动去重是最大优势
  3. 元素必须是不可变对象:比如可以加字符串、数字、元组,不能加列表

关键前置:可变与不可变对象

不管是字典的键还是集合的元素,都对“不可变”有要求,这里快速梳理一下区别:

可变对象(Mutable)

创建后内容可以直接修改,内存地址不变

  • 列表(list)、字典(dict)、普通集合(set)
lst = [3, 1, 2]
lst_id = id(lst)
lst.sort()  # 原地排序,不会生成新对象
print(id(lst) == lst_id)  # 输出: True

不可变对象(Immutable)

创建后内容不能直接修改,修改只能生成新对象

  • 字符串(str)、数字(int/float/bool)、元组(tuple)、frozenset(冻结集合,专门当键/集合元素用的不可变集合)
s = "hello"
s_id = id(s)
new_s = s.replace("h", "H")  # 生成新字符串,原字符串完全没变
print(id(s) == s_id)  # 输出: True
print(id(new_s) == s_id)  # 输出: False

实际应用场景建议

什么时候用dict?

  1. 键值对映射查询:比如根据学号查姓名、根据API请求的参数名取参数
  2. 数据分组统计:比如统计一段文本中每个单词的出现次数
  3. LRU/简单缓存:利用哈希表的O(1)查找速度实现快速缓存

什么时候用set?

  1. 批量去重:比如从用户提交的重复表单ID中提取唯一ID
  2. 高效成员关系判断:比如检查一个IP是否在黑名单里(比列表遍历快100倍以上)
  3. 简单集合逻辑:比如找两个用户的共同好友、找网站今日新增的独立访客

简单的性能对比(仅参考)

操作列表(list)字典(dict)/集合(set)
成员判断(10000元素)O(n) 慢O(1) 接近瞬时
插入/删除中间元素O(n) 慢O(1) 接近瞬时
内存占用多(为了哈希表预留空间)

高频小问题解答

Q: 为什么列表不能当字典的键或集合的元素? A: 因为列表是可变的!如果允许用列表当键,假设我们先存了d = {[1,2]: "a"},然后修改列表lst = [1,2][3,4],哈希值会变,原来的索引位置就找不到对应值了,还会造成内存泄漏。如果需要用类似序列的键,可以转成元组

# 可行:元组是不可变的
d = {(1, 2): "value"}

# 不可行:列表是可变的,会报TypeError
# d = {[1, 2]: "value"}

Q: Python 3.7+字典保持插入顺序了,还要OrderedDict吗? A: 分情况:

  • 只是想遍历顺序和插入一致:普通dict足够(3.7+)
  • 需要双向操作(比如把最后插入的键移到最前面) 或者兼容3.7以前的版本:必须用collections.OrderedDict