查看: 210|回复: 3

Python集合set去重与交并差关系运算实战及常见坑

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
集合(set)是 Python 里基于哈希表实现的内置类型,它用“无序 + 元素唯一”这两个约束换来了两个实用能力:一是天然去重,二是 O(1) 的成员查询与高效的交、并、差、对称差运算。只要代码里出现“统计不重复元素”“比对两份名单差异”“判断某值是否存在于大集合中”这类需求,集合通常都比列表更合适。本文围绕创建方式、增删查操作、关系运算、实战场景、集合推导式和 frozenset,把容易踩的坑一并说明。

一、集合的创建与两个基本特性

集合可以用字面量或 set() 构造,传入可迭代对象时会自动完成去重:
  1. s1 = {1, 2, 3}
  2. s2 = set([1, 2, 2, 3, 3])   # {1, 2, 3},重复元素被丢弃
  3. s3 = set("hello")           # {'h', 'e', 'l', 'o'},字符去重
  4. empty = set()               # 正确的空集合写法
复制代码

这里第一个要注意的点是:花括号 {} 创建的是空字典而不是空集合,空集合只能通过 set() 得到。

第二个特性是无序。集合没有下标,不能切片,也不能靠位置取值:
  1. s = {3, 1, 2}
  2. print(s)      # 输出 {1, 2, 3},但顺序不做保证
  3. s[0]          # TypeError: 'set' object is not subscriptable
复制代码

打印出来的顺序只是当前哈希布局的呈现,不要把它当作稳定行为写进业务逻辑。需要顺序就改用列表。

二、增删改查基本操作
  1. s = {1, 2, 3}
  2. s.add(4)            # 添加单个元素
  3. s.update([5, 6])    # 批量添加可迭代对象中的元素
  4. s.remove(1)         # 删除指定元素,不存在则抛 KeyError
  5. s.discard(100)      # 删除指定元素,不存在也不报错,推荐使用
  6. s.pop()             # 随机删除并返回一个元素
  7. s.clear()           # 清空集合
  8. 2 in s              # True,哈希查找,复杂度 O(1)
  9. len(s)              # 集合长度
复制代码

remove 与 discard 的区别是异常行为,这是实际调试中最常见的报错来源之一:批量清洗数据时如果无法确定元素一定存在,用 discard 更稳。pop() 因为无序,删除的是随机元素,不能依赖它删除“最后一个”。

三、关系运算:集合的核心价值

给定两个集合,四类运算的含义与结果如下:
  1. a = {1, 2, 3, 4}
  2. b = {3, 4, 5, 6}
  3. a & b    # {3, 4}            交集:两边都有的元素
  4. a | b    # {1, 2, 3, 4, 5, 6} 并集:合并后去重
  5. a - b    # {1, 2}            差集:a 有而 b 没有
  6. b - a    # {5, 6}            差集:b 有而 a 没有
  7. a ^ b    # {1, 2, 5, 6}      对称差集:只出现在一边的元素
复制代码

运算符写法简洁,但方法写法语义更明确,也便于链式调用:
  1. a.intersection(b)
  2. a.union(b)
  3. a.difference(b)
  4. a.symmetric_difference(b)
复制代码

判断集合之间关系还有三个方法:
  1. {1, 2}.issubset(a)            # True,是 a 的子集
  2. a.issuperset({1, 2})           # True,是 {1, 2} 的超集
  3. {1, 2}.isdisjoint({5, 6})      # True,两者无交集
复制代码

四、四个典型实战场景

场景 1:列表去重。最直接的做法是 set(lst),但顺序会被打乱。需要保序时用“记录已出现元素”的写法,或者利用 dict.fromkeys 保持插入顺序的特性(Python 3.7+):
  1. lst = [1, 2, 2, 3, 3, 3, 1]
  2. unique = list(set(lst))        # [1, 2, 3],顺序不保证
  3. seen = set()
  4. result = []
  5. for item in lst:
  6.     if item not in seen:
  7.         seen.add(item)
  8.         result.append(item)
  9. # result = [1, 2, 3],保持原顺序
  10. result = list(dict.fromkeys(lst))   # 一行实现保序去重
复制代码

场景 2:比对两份名单,用差集与交集直接算出新增、流失和共有项:
  1. yesterday = {"张三", "李四", "王五"}
  2. today = {"李四", "王五", "赵六"}
  3. print("新来的:", today - yesterday)     # {'赵六'}
  4. print("离开的:", yesterday - today)     # {'张三'}
  5. print("一直都在:", today & yesterday)   # {'李四', '王五'}
复制代码

场景 3:标签或兴趣匹配,交集大小除以并集大小即 Jaccard 相似度的思路:
  1. user_a_tags = {"Python", "摄影", "健身", "咖啡"}
  2. user_b_tags = {"Python", "旅游", "咖啡", "电影"}
  3. match = user_a_tags & user_b_tags
  4. print(f"共同兴趣:{match}")
  5. print(f"匹配度:{len(match) / len(user_a_tags | user_b_tags):.0%}")
复制代码

场景 4:高性能成员查找。判断一个值是否存在于百万级数据中,列表需要逐个遍历,集合走哈希直接定位:
  1. big_list = list(range(1_000_000))
  2. big_set = set(big_list)
  3. # 999999 in big_list  遍历比较,慢
  4. # 999999 in big_set   哈希查找,几乎瞬间返回
复制代码

代价是额外内存和一次建集合的开销,所以只在“数据量大且需要反复做 in 判断”时才值得转换,单次判断并不划算。

五、集合推导式

集合推导式语法与列表推导式一致,只是结果自动去重:
  1. {x ** 2 for x in range(5)}                 # {0, 1, 4, 9, 16}
  2. {c.lower() for c in ["A", "b", "A"]}      # {'a', 'b'}
  3. {w for w in text.split() if len(w) > 3}    # 长度大于 3 的单词
复制代码

六、frozenset:不可变集合

frozenset 与 set 的关系类似 tuple 与 list,创建后不能增删元素,因此可哈希,可以用作字典的 key,也能放进另一个集合:
  1. fs = frozenset([1, 2, 3])
  2. fs.add(4)                              # AttributeError,不可变
  3. d = {frozenset({1, 2}): "这一组"}       # 合法:frozenset 可作 key
复制代码

七、常见坑汇总

{} 得到的是空字典,空集合必须写 set();集合无序、不支持索引,需要顺序请用列表;set(lst) 去重会打乱顺序,保序用 list(dict.fromkeys(lst));集合元素必须可哈希,列表、字典、集合都不能作为元素,元组可以:
  1. {[1, 2], [3, 4]}    # TypeError: unhashable type: 'list'
  2. {(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。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python集合set去重与交并差关系运算实战及常见坑

感谢楼主,写得很系统,尤其是把 remove/discard 和 pop 的坑单独拎出来讲,很实用。我补充几个自己踩过的点,也算是给这个帖子加个小注脚。 第一,集合元素必须可哈希。往 set 里塞 list、dict 会直接 TypeError,得先转成 tuple 或者 frozenset。这个在“把列表里的列表去重”时特别容易撞上,比如 set([[1,2],[1,2]]) 就会报错,得写成 set(map(tuple, data)) 之类。 第二,运算符和方法在参数接受度上不完全等价。a & b 两边都必须是集合,但 a.intersection(b) 的参数可以是任意可迭代对象,a.intersection([3,4]) 能跑,a & [3,4] 就不行。用方法写法在做“集合和列表比对”时更省事,不用先转换。 第三,注意原地修改和新建的区别。a |= b、a -= b、a &= b 是原地更新,会改变 a 本身,如果 a 是函数参数或者被别处引用,就很容易产生意外的副作用;a = a | b 则是新建。团队协作里我更倾向用后者,除非明确为了省内存。 第四,frozenset 除了不可变,还能当 dict 的 key 或者放进另一个 set 里,做“多个集合去重”时挺有用。另外 sets 之间的 == 比较不看顺序,这点和列表不同,写测试断言时可以直接用。 第五,一个细节:判
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python集合set去重与交并差关系运算实战及常见坑

感谢楼主的整理,这篇结构很清楚,从特性到操作再到实战场景都串起来了。我自己经常踩的坑确实就是空集合那个,一开始写惯了 {},结果 debug 半天才发现是字典,后来统一用 set() 就没事了。remove 和 discard 的区分也很实用,批量清洗数据时用 remove 一旦遇到不存在的值就整个流程中断,换成 discard 稳很多。 保序去重那部分我也常用 dict.fromkeys 一行搞定,比手动维护 seen 集合要简洁,而且 Python 3.7 之后顺序有保证,用起来放心。Jaccard 那个例子把交集和并集组合起来算匹配度,思路很直观,标签匹配、推荐场景里都能直接套。 另外有个小地方想确认下:开头说会讲 frozenset,但正文里好像还没展开,是不是后面会补?我还挺想看看 frozenset 作为字典键或者嵌进集合里的用法。还有元素必须是可哈希对象这点,楼主有没有打算顺带提一句,像列表放进去会直接报 TypeError,新手也容易撞上。 场景 4 那个内存换查询速度的权衡说得很到位,单次判断确实没必要转集合,只有反复查询才划算。整体受教了,期待后续补充。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Python集合set去重与交并差关系运算实战及常见坑

这篇整理得挺全的,几个容易踩的点都点到了,尤其 {} 是空字典、remove 和 discard 的差别、pop 返回的是随机元素这几处,新手真的很容易栽。 顺着补几个我自己碰过的坑: 一是元素必须可哈希。往 set 里塞 list、dict、set 会直接报 TypeError,嵌套结构要先转成 tuple 或者 frozenset 才能放进去,这一点在去重嵌套数据时特别容易忘记。 二是运算符和方法的入参要求不一样。a & b、a | b 这种写法两边都必须是集合,a & [1, 2, 3] 会报错;但 a.intersection([1, 2, 3]) 可以接受任意可迭代对象,实际写代码时如果一边是列表,用方法会更省事。 三是布尔和数字会互相覆盖。集合里 True 和 1 被视为同一个元素,False 和 0 也是,因为哈希值一致。如果数据里混了布尔值和整数,去重结果可能会少一些看着不该少的项,排查起来挺费劲的。 四是差集方向要盯紧。a - b 和 b - a 结果完全不同,比对名单场景里“新增”和“流失”写反了不会报错,只会静默出错,建议变量命名就叫 added、removed 之类,别用 diff1、diff2。 五是 frozenset 除了不可变,更大的用处是能当字典的键或者放进另一个集合里,做分组归类的时候比 tuple 更合适。 保序去重那一段,dict.fromk
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-11-6 03:08 , Processed in 0.025052 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部