Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
1 | 作者:李晓辉 |
- Python 系列(一) 从零开始认识 Python,入门其实没那么难
- Python 系列(二) 学 Python 的第一步,把开发环境搭建好
- Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
- Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
- Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
- Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
- Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
- Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
- Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
- Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
- Python 系列(十一) list、tuple、set、dict,到底该怎么选?
- Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
- Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
- Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
如果一份服务器清单中出现了重复的主机名,怎么快速去重?
如果需要找出哪些服务器已经安装了 Nginx,哪些服务器还没有安装,又该怎么处理?
如果每天都要对比两份服务器清单,难道还要逐个检查吗?
别急,Python 的
set集合就能帮我们解决这些问题。它不仅能够自动去重,还能快速判断元素是否存在,更重要的是,它支持并集、交集、差集等集合运算。这一篇,我们就从 Linux 运维中的实际场景出发,学习 Python 的
set。
什么是 Set?
在前面的文章中,我们学习了 list 列表和 tuple 元组。
列表适合保存一组有序的数据,可以修改元素,也允许重复;元组同样有序,但创建后不能直接修改其中的元素。
那么,如果我们只关心数据中有哪些不同的元素,不关心它们的排列顺序,也不希望出现重复项,该怎么办?
例如,有这样一份服务器巡检记录:
1 | servers = [ |
其中,web-01 和 db-01 都出现了两次。
如果我们的目标是获得不重复的服务器名称,就可以使用 set。
1 | servers = [ |
输出示例:
1 | {'web-01', 'web-02', 'db-01', 'web-03'} |
注意,集合的输出顺序不固定,上面的结果只是一个示例。
原本列表中有 6 个元素,其中包含重复的服务器名称;转换成集合后,只剩下 4 个不同的元素。
这就是 set 最直观的特点:自动去重,而且不保证元素的顺序。
创建 Set 集合
创建集合主要有两种方式:使用 set() 构造函数,或者使用花括号 {} 直接定义元素。
使用 set() 创建集合
先来看最简单的情况。
1 | empty_set = set() |
输出:
1 | set() |
这里需要注意一个容易踩坑的地方。
虽然 Python 使用 {} 表示集合字面量,但空的 {} 创建的是字典,而不是集合。
1 | a = {} |
输出:
1 | <class 'dict'> |
所以,如果需要创建一个空集合,应该使用 set()。
set() 还可以接收列表、元组、字符串等可迭代对象,并将其中不重复的元素放入集合。
1 | names = ["Tom", "Jerry", "Tom", "Alice"] |
输出示例:
1 | {'Jerry', 'Alice', 'Tom'} |
如果传入的是字符串,Python 会把字符串中的每个字符当作一个独立元素。
1 | letters = set("mississippi") |
输出示例:
1 | {'m', 'i', 's', 'p'} |
字符串 mississippi 中虽然有很多重复字符,但集合只会保留不同的字符。
使用花括号创建集合
如果已经知道集合中有哪些元素,也可以直接使用花括号。
1 | weekdays = { |
输出示例:
1 | {'Sun', 'Mon', 'Sat', 'Tue', 'Fri', 'Wed', 'Thu'} |
即使在代码中按照某个顺序填写元素,集合也不承诺按照这个顺序保存或输出。
因此,如果业务要求按照服务器名称、时间或者优先级排列数据,就不要依赖 set 的顺序。需要排序时,可以使用 sorted()。
1 | servers = {"web-03", "web-01", "web-02"} |
输出:
1 | ['web-01', 'web-02', 'web-03'] |
这里返回的是列表,而不是集合。
Set 中的元素有什么限制?
集合中的元素必须是可哈希的(hashable),因此常见的字符串、整数等可以作为集合元素,但列表和字典不能直接放进集合。
例如:
1 | servers = {"web-01", "web-02"} |
输出
1 | {'web-02', 'web-01'} |
这些写法没有问题。
但是下面的代码会报错:
1 | items = {[1, 2], [3, 4]} |
因为列表是可变对象,不能直接作为集合元素。
1 | Traceback (most recent call last): |
如果确实需要把一组固定的值作为集合元素,可以考虑使用元组,前提是元组内部的所有元素也都可哈希。
1 | items = {(1, 2), (3, 4)} |
输出示例:
1 | {(1, 2), (3, 4)} |
如何判断元素是否存在?
在 Linux 运维中,我们经常需要判断某台服务器是否已经出现在巡检清单中,或者某个服务是否属于预期的服务列表。
如果使用列表,可以通过 in 判断元素是否存在;集合同样支持这个操作。
1 | servers = {"web-01", "web-02", "db-01"} |
输出:
1 | True |
其中:
in:判断元素是否存在。not in:判断元素是否不存在。
如果服务器名称存在,结果就是 True;否则就是 False。
那么,为什么不直接使用列表呢?
如果只是几台服务器,使用列表来判断某台服务器是否存在,完全没有问题。
但如果需要检查几千台甚至几万台服务器,并且反复判断某个服务器名称是否在清单中,使用集合通常会更高效。
这是因为,列表查找就像翻阅一本没有目录的名单,通常需要从头开始,一个一个地查找;而集合更像是利用哈希机制快速定位目标,通常不需要逐个比较所有元素。
因此,数据越多、查找越频繁,集合在查找效率上的优势就越明显。
Set 的常用方法
了解集合的创建和查找之后,再来看如何修改集合中的元素。
add():添加一个元素
如果需要向集合中添加一个元素,可以使用 add()。
1 | servers = {"web-01", "web-02"} |
输出示例:
1 | {'web-02', 'web-01', 'web-03'} |
这里连续调用了两次 add()。
第一次添加 web-03,因为它原本不存在,所以集合中增加了一个元素。
第二次添加 web-01,但它原本就存在,因此集合不会增加重复项。
这意味着,即使不提前判断元素是否存在,也可以直接使用 add() 完成去重添加。
update():批量添加元素
如果需要一次添加多个元素,可以使用 update()。
1 | servers = {"web-01", "web-02"} |
输出示例:
1 | {'web-02', 'db-01', 'web-01', 'web-03'} |
update() 会遍历传入的可迭代对象,将其中的元素逐个添加到集合中。
即使新数据里存在重复元素,或者与原集合中的元素重复,也不会产生重复项。
它还可以接收多个可迭代对象:
1 | servers = {"web-01"} |
输出示例:
1 | {'db-01', 'web-01', 'web-02', 'web-03', 'db-02'} |
注意,update() 修改的是原集合,不会返回一个新的集合供你接收。
remove() 和 discard():删除元素
如果需要从集合中删除元素,可以使用 remove() 或 discard()。
两者看起来很像,但存在一个重要区别。
1 | servers = {"web-01", "web-02", "db-01"} |
输出示例:
1 | {'db-01', 'web-01'} |
这里,remove() 删除了存在的 web-02;discard() 尝试删除不存在的 web-03,但没有报错。
如果使用 remove() 删除一个不存在的元素,就会引发 KeyError。
1 | servers = {"web-01", "web-02"} |
错误示例:
1 | Traceback (most recent call last): |
而 discard() 不会因为元素不存在而报错。
因此,在自动化运维脚本中,如果无法确定元素是否存在,而且删除失败不需要触发异常,使用 discard() 往往更方便。
pop():删除并返回一个元素
集合还提供了 pop() 方法。
1 | servers = {"web-01", "web-02", "db-01"} |
输出示例:
1 | Deleted: web-02 |
这里的 web-02 只是示例,实际删除哪个元素取决于集合本身的状态,不能依赖某个固定顺序。
如果集合为空,调用 pop() 会引发 KeyError。
因此,pop() 不适合用来删除指定的服务器名称。如果目标是删除某个确定的元素,应当使用 remove() 或 discard()。
clear():清空集合
如果需要一次删除集合中的所有元素,可以使用 clear()。
1 | servers = {"web-01", "web-02", "db-01"} |
输出:
1 | set() |
执行 clear() 后,原集合仍然存在,只是其中不再包含任何元素。
集合之间的关系:子集与超集
除了添加、删除和查找,集合还可以用来判断两个数据集合之间的包含关系。
例如,某次巡检计划需要检查以下服务器:
1 | planned_servers = { |
实际完成巡检的服务器是:
1 | checked_servers = { |
显然,实际完成巡检的服务器都属于计划清单,但计划清单中的服务器并没有全部完成检查。
这时可以使用 issubset() 和 issuperset()。
1 | print(checked_servers.issubset(planned_servers)) |
输出:
1 | True |
这两个结果都是 True,并不矛盾。
checked_servers.issubset(planned_servers):判断实际巡检集合是否为计划集合的子集。planned_servers.issuperset(checked_servers):判断计划集合是否为实际巡检集合的超集。
如果想检查计划中的服务器是否全部完成巡检,可以进一步判断:
1 | print(planned_servers.issubset(checked_servers)) |
输出:
1 | False |
因为 db-01 尚未出现在实际巡检集合中。
如果两个集合包含的元素完全相同,那么它们既是对方的子集,也是对方的超集。
集合运算:并集、交集、差集与对称差集
接下来是 set 非常实用的一项能力:集合运算。
假设我们有两份服务器清单:
1 | web_servers = {"web-01", "web-02", "web-03"} |
web_servers 表示业务侧登记的 Web 服务器,nginx_servers 表示已经确认安装 Nginx 的服务器。
我们希望回答几个问题:
- 两份清单合起来,一共有多少台不同的服务器?
- 哪些服务器同时出现在两份清单中?
- 哪些服务器只出现在第一份清单中?
- 哪些服务器只出现在其中一份清单中?
这些问题都可以通过集合运算解决。
并集:合并两份清单
并集使用 | 运算符,也可以使用 union() 方法。
1 | all_servers = web_servers | nginx_servers |
输出示例:
1 | {'web-02', 'web-01', 'web-04', 'web-03'} |
并集会收集两个集合中的所有不同元素。
即使 web-02 和 web-03 同时出现在两份清单中,结果也只会保留一份。
使用方法调用也可以得到相同结果:
1 | all_servers = web_servers.union(nginx_servers) |
输出示例:
1 | {'web-02', 'web-01', 'web-04', 'web-03'} |
交集:找出共同元素
交集使用 & 运算符,也可以使用 intersection() 方法。
1 | common_servers = web_servers & nginx_servers |
输出:
1 | {'web-02', 'web-03'} |
交集只保留同时存在于两个集合中的元素。
在运维工作中,可以利用它找出两份清单中重复出现的服务器,或者核对某些主机是否同时满足两组条件。
差集:找出缺少的元素
差集使用 - 运算符,也可以使用 difference() 方法。
1 | print(web_servers - nginx_servers) |
输出:
1 | {'web-01'} |
这里需要特别注意:差集与操作数的顺序有关。
web_servers - nginx_servers 表示存在于 web_servers,但不存在于 nginx_servers 中的元素。
反过来,nginx_servers - web_servers 则表示只存在于 nginx_servers 中的元素。
这在日常运维中非常实用。例如,想找出登记在业务清单中、但尚未确认安装 Nginx 的服务器,就可以通过第一种差集得到结果。
当然,这个判断依赖于两份清单的数据准确性;差集只能告诉我们数据之间的差异,并不能单凭结果证明服务器上实际运行了什么服务。
对称差集:找出双方不共有的元素
对称差集使用 ^ 运算符,也可以使用 symmetric_difference() 方法。
1 | different_servers = web_servers ^ nginx_servers |
输出:
1 | {'web-01', 'web-04'} |
对称差集会保留只出现在其中一个集合中的元素,而排除双方共有的元素。
可以把它理解为:先把两个集合合并,再去掉交集中的元素。
如果需要比较两份服务器清单,找出双方存在差异的部分,对称差集就很方便。
集合运算会修改原集合吗?
前面使用 |、&、- 和 ^ 得到的都是新集合,原来的集合不会因此改变。
1 | a = {"web-01", "web-02"} |
输出示例:
1 | a: {'web-02', 'web-01'} |
如果希望直接修改原集合,也可以使用对应的增强赋值运算符。
1 | a = {"web-01", "web-02"} |
输出示例:
1 | {'web-02', 'web-01', 'web-03'} |
a |= b 相当于将并集结果更新到 a 中。类似地,还有 &=、-= 和 ^=,分别用于原地更新交集、差集和对称差集。
实战:利用 Set 对比服务器清单
前面分别学习了集合的创建、去重、成员查找和集合运算。现在把它们放到一个更接近 Linux 运维的场景中。
假设我们维护着一份业务服务器清单,同时还有一份已经完成 Nginx 检查的服务器清单。
我们希望自动完成以下工作:
- 去除服务器清单中的重复名称。
- 找出已经完成检查的服务器。
- 找出尚未完成检查的服务器。
- 找出出现在检查记录中、但不在业务清单里的服务器。
- 计算检查覆盖率。
可以这样实现:
1 | #!/usr/bin/env python3 |
输出:
1 | 业务服务器总数: 4 |
这个例子把集合的几种核心能力串联了起来。
业务清单中有 5 条记录,但 web-02 出现了两次,因此去重后只有 4 台不同的服务器。
已经完成检查的服务器有 web-01、web-03 和 db-02。其中,前两台属于业务清单,db-02 则是额外出现在检查记录中的服务器。
最终,我们不仅知道哪些服务器尚未完成检查,还能发现清单之间的不一致,并计算出检查覆盖率。
需要注意的是,这里的“已完成检查”只是示例数据,并没有真正连接服务器执行命令。实际使用时,可以把 SSH 巡检结果、监控平台数据或者云平台 API 返回的数据转换成集合,再进行对比。
Set 与 List,应该如何选择?
学习到这里,你可能会问:既然 set 可以去重、查找和比较数据,是不是以后都应该使用 set?
当然不是。不同的数据结构有不同的适用场景。
| 对比项 | List 列表 | Set 集合 |
|---|---|---|
| 是否保证顺序 | 是 | 否 |
| 是否允许重复元素 | 允许 | 不允许 |
| 是否支持索引访问 | 支持 | 不支持 |
| 是否支持成员查找 | 支持 in | 支持 in |
| 是否支持集合运算 | 不直接支持 | 原生支持 |
| 是否适合保存重复记录 | 适合 | 不适合 |
例如,保存一份按时间顺序排列的巡检记录,应该优先考虑列表,因为同一台服务器可能在不同时间被检查多次,每条记录都可能有意义。
如果只关心有哪些不同的服务器,或者需要快速判断某台服务器是否存在、对比两份清单,那么集合通常更加合适。
还有一点需要记住:set 是可变的,但集合中的元素必须是可哈希的。集合本身不能直接通过索引访问元素,也不能像列表那样使用 append() 添加数据。
Set 常用操作速查
| 操作 | 作用 |
|---|---|
set() | 创建空集合,或将可迭代对象转换为集合 |
{1, 2, 3} | 创建包含元素的集合 |
x in s | 判断元素是否存在 |
s.add(x) | 添加一个元素 |
s.update(iterable) | 批量添加元素 |
s.remove(x) | 删除元素,不存在时抛出 KeyError |
s.discard(x) | 删除元素,不存在时不报错 |
s.pop() | 删除并返回一个元素 |
s.clear() | 清空集合 |
a.issubset(b) | 判断 a 是否为 b 的子集 |
a.issuperset(b) | 判断 a 是否为 b 的超集 |
a | b | 并集 |
a & b | 交集 |
a - b | 差集 |
a ^ b | 对称差集 |
写在最后
这一篇,我们学习了 Python 的 set 集合。
它最核心的特点可以概括为:元素不重复、不保证顺序,并且支持高效的成员查找与集合运算。
在 Linux 运维自动化中,集合尤其适合处理服务器清单去重、巡检结果对比、服务清单核对以及配置差异分析等问题。
不过,集合并不能代替所有数据结构。如果需要保留顺序、重复记录或通过索引访问元素,列表依然是更合适的选择。
下一篇,我们将学习 Python 中另一种非常重要的数据结构:dict 字典。它通过键值对组织数据,能够帮助我们更方便地保存服务器信息、读取配置,以及根据服务器名称快速查找对应的数据。
