前言
先说一个措辞问题。这个标题里的「容器」和「集合」其实是两个层级的概念,不该并列。容器(container)是上位概念,指任何用来装一组对象的类型,Python 内置的list、tuple、dict、set、str都属于容器;而集合(set)是其中一个具体的类型,特指无序、元素唯一的那种容器。标题写成「常用容器(集合)」,容易让人误以为「容器就等于集合」。准确的表述应该是「常用容器,含集合」。
顺带说一句:英文里collection也常被翻译成「集合」,那是指更宽泛的「一组对象」,和set这个具体类型不是一回事。collections模块名字里的那个collections就是前者。
本文按「四类内置容器逐个过一遍 + 横向对比 + 各自示例」的结构,把list、tuple、dict、set一次讲清,重点放在set上。
一、四个内置容器总览
先看一张总表,把四者的关键差别对齐。
| 容器 | 字面量写法 | 可变 | 有序 | 可哈希 | 成员判断 | 典型场景 |
|---|
list | [1, 2] | 是 | 是 | 否 | O(n) | 有序、可变、允许重复的数据 |
tuple | (1, 2) | 否 | 是 | 元素可哈希时是 | O(n) | 固定记录、当字典键 |
dict | {"a": 1} | 是 | 3.7 起保证插入顺序 | 否 | O(1) 平均 | 键值映射 |
set | {1, 2} | 是 | 否 | 否 | O(1) 平均 | 去重、存在性判断 |
几个必须记住的细节:
{}是空字典,空集合只能写set();空列表是[],空元组是()。dict和set的成员判断都是哈希定位,平均 O(1);list和tuple要逐个比。dict的键必须可哈希,set的元素也必须可哈希,它们的内核是同一套哈希表。tuple不可变,所以元素都可哈希时整体可哈希;list可变,永远不可哈希。
二、list:最常用的可变序列
列表按位置组织数据,支持下标、切片、增删改,允许重复元素。
# 适用于 Python 3.8+
fruits = ["apple", "banana", "cherry"]
fruits.append("date") # 尾部追加
fruits.insert(0, "avocado") # 指定位置插入
fruits.remove("banana") # 按值删除,不存在则抛 ValueError
print(fruits) # ['avocado', 'apple', 'cherry', 'date']
print(fruits[1]) # apple
print(sorted(fruits)) # 返回排序后的新列表
# 注意 sort() 原地排序并返回 None
ret = fruits.sort()
print(ret) # None
print(fruits) # 排序后的列表本身最后三行是一个高频坑:list.sort()原地排序、返回None,而内置函数sorted()返回新列表。把fruits = fruits.sort()写出来,fruits就变成None了。
三、tuple:不可变序列
元组和列表的读取方式一样,区别在于不能改。
# 适用于 Python 3.8+
point = (3, 5)
print(point[0], point[1]) # 3 5
# 单元素元组的逗号不能省
one = (1,)
print(type(one).__name__) # tuple
print(type((1)).__name__) # int,这个是整数
# 解包
x, y = point
print(x, y) # 3 5元组只有count和index两个专属方法。因为它不可变,所以能被当成字典的键、放进集合,这是列表做不到的。
四、dict:键值映射
字典保存键到值的映射,从 3.7 起保证插入顺序(3.6 只是实现细节,不要依赖 3.6 的行为)。
# 适用于 Python 3.8+
student = {"name": "小明", "age": 12}
student["grade"] = 6 # 新增键
student["age"] = 13 # 覆盖已有键
print(student["name"]) # 小明
print(student.get("score")) # None,键不存在返回 None
print(student.get("score", 0)) # 0,可以给默认值
print(list(student.keys())) # ['name', 'age', 'grade'],插入顺序d[key]在键不存在时抛KeyError,d.get(key)返回None——这是两者最重要的区别。另外,从 3.9 起有|合并运算符(PEP 584),3.8 及更早版本要用{**a, **b}。
# 适用于 Python 3.9+
other = {"city": "北京"}
merged = student | other
print(sorted(merged.keys())) # ['age', 'city', 'grade', 'name']# 适用于 Python 3.8,替代 3.9 的 | 合并运算符
other = {"city": "北京"}
merged = {**student, **other}
print(sorted(merged.keys())) # ['age', 'city', 'grade', 'name']五、set:无序且唯一
集合放进去的元素唯一、无序。它的两个核心用途是去重和快速判断存在。
# 适用于 Python 3.8+
# 去重
tags = ["python", "web", "python", "async"]
unique = set(tags)
print(sorted(unique)) # ['async', 'python', 'web']
# 空集合必须用 set()
empty = set()
print(type(empty).__name__) # set
# 成员判断,平均 O(1)
print("web" in unique) # True
# 集合运算
a = {"python", "async", "web"}
b = {"python", "packaging"}
print(sorted(a & b)) # ['python'],交集
print(sorted(a | b)) # 并集
print(sorted(a - b)) # 差集集合要求元素可哈希:数字、字符串、bytes、frozenset可以;tuple在内部元素都可哈希时可以;list、dict、set不行。
# 适用于 Python 3.8+
print(set((1, 2, 3))) # {(1, 2, 3)},元组可哈希
print(set([1, 2])) # {1, 2},列表是「输入」而不是「元素」
try:
bad = {[1, 2], [3, 4]}
except TypeError as e:
print("报错:", e) # unhashable type: 'list'
# 换成元组就合法,也能表示「集合的集合」
good = {(1, 2), (3, 4)}
print(len(good)) # 2这两段的对比值得琢磨:set(某列表)是把列表当输入,集合会逐个取出列表里的元素,所以它当然可以是不可哈希的东西;而{某列表}是把列表当元素,这时才要求可哈希。
六、横向对比示例
下面用同一份数据分别用四种容器处理,直观感受取舍。
# 适用于 Python 3.8+
raw = ["alice", "bob", "alice", "carol"]
as_list = list(raw) # 保留全部,含重复
as_tuple = tuple(raw) # 同上,但不可变
as_counts = {} # 统计出现次数
for name in raw:
as_counts[name] = as_counts.get(name, 0) + 1
as_set = set(raw) # 只留唯一值
print(as_list) # ['alice', 'bob', 'alice', 'carol']
print(as_tuple[0]) # alice
print(as_counts) # {'alice': 2, 'bob': 1, 'carol': 1}
print(sorted(as_set)) # ['alice', 'bob', 'carol']
# 顺序敏感的去重(3.7 起 dict 保序)
ordered_unique = list(dict.fromkeys(raw))
print(ordered_unique) # ['alice', 'bob', 'carol']逐行推演:raw有四个元素,alice出现两次。as_list原样保留。as_counts用get(name, 0) + 1累加,得到每个名字的次数。as_set去掉重复,只剩三个。最后的dict.fromkeys(raw)按顺序建字典,键就是去重后的名字,再转回列表就得到保序去重结果——这是「既要去重又要保序」的标准写法,依赖的是字典 3.7 起保证插入顺序这条官方承诺。
七、实战:统计标签并比较两组
# 适用于 Python 3.8+
def tag_stats(rows):
"""rows 是 (文章标题, 标签列表) 的列表,统计每个标签出现次数。"""
counts = {}
for title, tag_list in rows:
for tag in tag_list:
counts[tag] = counts.get(tag, 0) + 1
return counts
def common_tags(rows_a, rows_b):
"""找出两组文章共有的标签。"""
a = {tag for _, tags in rows_a for tag in tags}
b = {tag for _, tags in rows_b for tag in tags}
return sorted(a & b), sorted(a - b), sorted(b - a)
if __name__ == "__main__":
group_a = [("t1", ["python", "web"]), ("t2", ["python", "test"])]
group_b = [("t3", ["python", "packaging"])]
stats = tag_stats(group_a)
print(sorted(stats.items())) # [('python', 2), ('test', 1), ('web', 1)]
shared, only_a, only_b = common_tags(group_a, group_b)
print("共有:", shared) # 共有: ['python']
print("只在 A:", only_a) # 只在 A: ['test', 'web']
print("只在 B:", only_b) # 只在 B: ['packaging']逐行推演:tag_stats用字典累加每个标签的出现次数,counts.get(tag, 0)在标签首次出现时给 0。common_tags用集合推导式把两组的标签分别收集成集合,再做交集和两个方向的差集。sorted(stats.items())把键值对按元组排序后打印。整段只用到 dict 和 set 的基础用法。
常见坑点
坑点 1:把{}当成空集合。❌s = {}; s.add(1)——{}是空字典,抛AttributeError: 'dict' object has no attribute 'add'。 ✅ 空集合写s = set()。
坑点 2:把list.sort()的返回值赋回去。❌items = items.sort()——原地排序返回None,items变成None。 ✅ 直接items.sort();要新列表就用items = sorted(items)。
坑点 3:dict用[]取不存在的键。❌d["missing"]——抛KeyError。 ✅ 允许缺省时用d.get("missing")或带默认值的d.get("missing", 0)。
坑点 4:以为集合保持插入顺序。❌list(set(["b", "a"]))期待得到["b", "a"]——集合无序。 ✅ 保序去重用list(dict.fromkeys(["b", "a"])),得到["b", "a"]。
坑点 5:把不可哈希对象放进集合或当字典键。❌{[1, 2]}或d = {[1, 2]: "x"}——抛TypeError: unhashable type: 'list'。 ✅ 换成元组或字符串:{(1, 2)}、d = {(1, 2): "x"}。
坑点 6:以为set(某列表)会因为列表不可哈希而报错。❌ 看到「集合元素必须可哈希」就以为set([1, 2])非法——它完全合法,参数是被迭代的对象,不是元素。 ✅ 分清「作为输入」和「作为元素」:set([1, 2])得到{1, 2};{[1, 2]}才报错。
坑点 7:用+拼接集合。❌{1} + {2}——集合不支持+,抛TypeError。 ✅ 用并集:{1} | {2}或{1}.union({2}),得到{1, 2}。
坑点 8:以为dict和set的in也很快却用错了对象。❌"x" in ["a", "b", ...]对长列表反复判断——这是 O(n) 扫描。 ✅ 先把列表转成集合再判断:"x" in set(big_list),之后每次判断平均 O(1)。
总结
| 需求 | 选哪个 | 关键理由 |
|---|
| 有序、会增删改 | list | 可变、支持下标与切片 |
| 固定不变的一组值 | tuple | 不可变,可当键和集合元素 |
| 键到值的映射 | dict | 平均 O(1) 按键查找,3.7 起保序 |
| 去重 / 判断存在 / 集合运算 | set | 元素唯一,平均 O(1) 成员判断 |
| 需要可哈希的集合 | frozenset | 不可变所以可哈希 |
「容器」是一个大类,「集合」只是其中一员。选容器时按顺序问三个问题:需要键值映射吗(dict)?需要去重或纯存在性判断吗(set)?需要之后修改吗(list / tuple)?三个问题问完,选型基本就定了。