集合(set)是 Python 里基于哈希表实现的内置类型,它用“无序 + 元素唯一”这两个约束换来了两个实用能力:一是天然去重,二是 O(1) 的成员查询与高效的交、并、差、对称差运算。只要代码里出现“统计不重复元素”“比对两份名单差异”“判断某值是否存在于大集合中”这类需求,集合通常都比列表更合适。本文围绕创建方式、增删查操作、关系运算、实战场景、集合推导式和 frozenset,把容易踩的坑一并说明。
一、集合的创建与两个基本特性
集合可以用字面量或 set() 构造,传入可迭代对象时会自动完成去重:
- s1 = {1, 2, 3}
- s2 = set([1, 2, 2, 3, 3]) # {1, 2, 3},重复元素被丢弃
- s3 = set("hello") # {'h', 'e', 'l', 'o'},字符去重
- empty = set() # 正确的空集合写法
复制代码
这里第一个要注意的点是:花括号 {} 创建的是空字典而不是空集合,空集合只能通过 set() 得到。
第二个特性是无序。集合没有下标,不能切片,也不能靠位置取值:
- s = {3, 1, 2}
- print(s) # 输出 {1, 2, 3},但顺序不做保证
- s[0] # TypeError: 'set' object is not subscriptable
复制代码
打印出来的顺序只是当前哈希布局的呈现,不要把它当作稳定行为写进业务逻辑。需要顺序就改用列表。
二、增删改查基本操作
- s = {1, 2, 3}
- s.add(4) # 添加单个元素
- s.update([5, 6]) # 批量添加可迭代对象中的元素
- s.remove(1) # 删除指定元素,不存在则抛 KeyError
- s.discard(100) # 删除指定元素,不存在也不报错,推荐使用
- s.pop() # 随机删除并返回一个元素
- s.clear() # 清空集合
- 2 in s # True,哈希查找,复杂度 O(1)
- len(s) # 集合长度
复制代码
remove 与 discard 的区别是异常行为,这是实际调试中最常见的报错来源之一:批量清洗数据时如果无法确定元素一定存在,用 discard 更稳。pop() 因为无序,删除的是随机元素,不能依赖它删除“最后一个”。
三、关系运算:集合的核心价值
给定两个集合,四类运算的含义与结果如下:
- a = {1, 2, 3, 4}
- b = {3, 4, 5, 6}
- a & b # {3, 4} 交集:两边都有的元素
- a | b # {1, 2, 3, 4, 5, 6} 并集:合并后去重
- a - b # {1, 2} 差集:a 有而 b 没有
- b - a # {5, 6} 差集:b 有而 a 没有
- a ^ b # {1, 2, 5, 6} 对称差集:只出现在一边的元素
复制代码
运算符写法简洁,但方法写法语义更明确,也便于链式调用:
- a.intersection(b)
- a.union(b)
- a.difference(b)
- a.symmetric_difference(b)
复制代码
判断集合之间关系还有三个方法:
- {1, 2}.issubset(a) # True,是 a 的子集
- a.issuperset({1, 2}) # True,是 {1, 2} 的超集
- {1, 2}.isdisjoint({5, 6}) # True,两者无交集
复制代码
四、四个典型实战场景
场景 1:列表去重。最直接的做法是 set(lst),但顺序会被打乱。需要保序时用“记录已出现元素”的写法,或者利用 dict.fromkeys 保持插入顺序的特性(Python 3.7+):
- lst = [1, 2, 2, 3, 3, 3, 1]
- unique = list(set(lst)) # [1, 2, 3],顺序不保证
- seen = set()
- result = []
- for item in lst:
- if item not in seen:
- seen.add(item)
- result.append(item)
- # result = [1, 2, 3],保持原顺序
- result = list(dict.fromkeys(lst)) # 一行实现保序去重
复制代码
场景 2:比对两份名单,用差集与交集直接算出新增、流失和共有项:
- yesterday = {"张三", "李四", "王五"}
- today = {"李四", "王五", "赵六"}
- print("新来的:", today - yesterday) # {'赵六'}
- print("离开的:", yesterday - today) # {'张三'}
- print("一直都在:", today & yesterday) # {'李四', '王五'}
复制代码
场景 3:标签或兴趣匹配,交集大小除以并集大小即 Jaccard 相似度的思路:
- user_a_tags = {"Python", "摄影", "健身", "咖啡"}
- user_b_tags = {"Python", "旅游", "咖啡", "电影"}
- match = user_a_tags & user_b_tags
- print(f"共同兴趣:{match}")
- print(f"匹配度:{len(match) / len(user_a_tags | user_b_tags):.0%}")
复制代码
场景 4:高性能成员查找。判断一个值是否存在于百万级数据中,列表需要逐个遍历,集合走哈希直接定位:
- big_list = list(range(1_000_000))
- big_set = set(big_list)
- # 999999 in big_list 遍历比较,慢
- # 999999 in big_set 哈希查找,几乎瞬间返回
复制代码
代价是额外内存和一次建集合的开销,所以只在“数据量大且需要反复做 in 判断”时才值得转换,单次判断并不划算。
五、集合推导式
集合推导式语法与列表推导式一致,只是结果自动去重:
- {x ** 2 for x in range(5)} # {0, 1, 4, 9, 16}
- {c.lower() for c in ["A", "b", "A"]} # {'a', 'b'}
- {w for w in text.split() if len(w) > 3} # 长度大于 3 的单词
复制代码
六、frozenset:不可变集合
frozenset 与 set 的关系类似 tuple 与 list,创建后不能增删元素,因此可哈希,可以用作字典的 key,也能放进另一个集合:
- fs = frozenset([1, 2, 3])
- fs.add(4) # AttributeError,不可变
- d = {frozenset({1, 2}): "这一组"} # 合法:frozenset 可作 key
复制代码
七、常见坑汇总
{} 得到的是空字典,空集合必须写 set();集合无序、不支持索引,需要顺序请用列表;set(lst) 去重会打乱顺序,保序用 list(dict.fromkeys(lst));集合元素必须可哈希,列表、字典、集合都不能作为元素,元组可以:
- {[1, 2], [3, 4]} # TypeError: unhashable type: 'list'
- {(1, 2), (3, 4)} # 合法,元组可哈希
复制代码
另外 pop() 删除的是随机元素,别把它当“删末尾”使用。
八、练习题
1. 对列表 [1, 1, 2, 2, 3, 4, 4, 5] 用两种方式去重:一种不保序(set),一种保序(dict.fromkeys 或手动记录)。
2. 求两个字符串中共同出现的字符,例如 "hello" 与 "world" 得到 {'l', 'o'},用 set 的交集实现。
3. 随机生成 100 个 1 到 50 的整数,用 len(set(...)) 统计其中不同数字的个数。
小结
集合由“无序 + 唯一”两个特性定义,空集合用 set() 创建;& 交集、| 并集、- 差集、^ 对称差是它最有价值的运算;去重最快用 set(lst),保序去重与 list(dict.fromkeys(lst));大量 in 查询应优先使用集合而非列表;需要不可变、可哈希的集合就换成 frozenset。 |