python语言中的常用容器(集合)示例详解
前言先说一个措辞问题。这个标题里的「容器」和「集合」其实是两个层级的概念不该并列。容器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.8fruits [apple, banana, cherry]fruits.append(date) # 尾部追加fruits.insert(0, avocado) # 指定位置插入fruits.remove(banana) # 按值删除不存在则抛 ValueErrorprint(fruits) # [avocado, apple, cherry, date]print(fruits[1]) # appleprint(sorted(fruits)) # 返回排序后的新列表# 注意 sort() 原地排序并返回 Noneret fruits.sort()print(ret) # Noneprint(fruits) # 排序后的列表本身最后三行是一个高频坑list.sort()原地排序、返回None而内置函数sorted()返回新列表。把fruits fruits.sort()写出来fruits就变成None了。三、tuple不可变序列元组和列表的读取方式一样区别在于不能改。# 适用于 Python 3.8point (3, 5)print(point[0], point[1]) # 3 5# 单元素元组的逗号不能省one (1,)print(type(one).__name__) # tupleprint(type((1)).__name__) # int这个是整数# 解包x, y pointprint(x, y) # 3 5元组只有count和index两个专属方法。因为它不可变所以能被当成字典的键、放进集合这是列表做不到的。四、dict键值映射字典保存键到值的映射从 3.7 起保证插入顺序3.6 只是实现细节不要依赖 3.6 的行为。# 适用于 Python 3.8student {name: 小明, age: 12}student[grade] 6 # 新增键student[age] 13 # 覆盖已有键print(student[name]) # 小明print(student.get(score)) # None键不存在返回 Noneprint(student.get(score, 0)) # 0可以给默认值print(list(student.keys())) # [name, age, grade]插入顺序d[key]在键不存在时抛KeyErrord.get(key)返回None——这是两者最重要的区别。另外从 3.9 起有|合并运算符PEP 5843.8 及更早版本要用{**a, **b}。# 适用于 Python 3.9other {city: 北京}merged student | otherprint(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.8print(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.8raw [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) 1as_set set(raw) # 只留唯一值print(as_list) # [alice, bob, alice, carol]print(as_tuple[0]) # aliceprint(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.8def tag_stats(rows):rows 是 (文章标题, 标签列表) 的列表统计每个标签出现次数。counts {}for title, tag_list in rows:for tag in tag_list:counts[tag] counts.get(tag, 0) 1return countsdef 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()——原地排序返回Noneitems变成None。 ✅ 直接items.sort()要新列表就用items sorted(items)。坑点 3dict用[]取不存在的键。❌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三个问题问完选型基本就定了。