1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Python 系列(一) 从零开始认识 Python,入门其实没那么难
  2. Python 系列(二) 学 Python 的第一步,把开发环境搭建好
  3. Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
  4. Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
  5. Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
  6. Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
  7. Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
  8. Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
  9. Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
  10. Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
  11. Python 系列(十一) list、tuple、set、dict,到底该怎么选?
  12. Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
  13. Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
  14. Python 系列(十四) Set 集合实战,搞定数据去重与集合运算

如果一份服务器清单中出现了重复的主机名,怎么快速去重?

如果需要找出哪些服务器已经安装了 Nginx,哪些服务器还没有安装,又该怎么处理?

如果每天都要对比两份服务器清单,难道还要逐个检查吗?

别急,Python 的 set 集合就能帮我们解决这些问题。它不仅能够自动去重,还能快速判断元素是否存在,更重要的是,它支持并集、交集、差集等集合运算。

这一篇,我们就从 Linux 运维中的实际场景出发,学习 Python 的 set。

什么是 Set?

在前面的文章中,我们学习了 list 列表和 tuple 元组。

列表适合保存一组有序的数据,可以修改元素,也允许重复;元组同样有序,但创建后不能直接修改其中的元素。

那么,如果我们只关心数据中有哪些不同的元素,不关心它们的排列顺序,也不希望出现重复项,该怎么办?

例如,有这样一份服务器巡检记录:

1
2
3
4
5
6
7
8
servers = [
"web-01",
"web-02",
"db-01",
"web-01",
"web-03",
"db-01"
]

其中,web-01 和 db-01 都出现了两次。

如果我们的目标是获得不重复的服务器名称,就可以使用 set。

1
2
3
4
5
6
7
8
9
10
11
12
13
servers = [
"web-01",
"web-02",
"db-01",
"web-01",
"web-03",
"db-01"
]

unique_servers = set(servers)

print(unique_servers)
print(len(unique_servers))

输出示例:

1
2
{'web-01', 'web-02', 'db-01', 'web-03'}
4

注意,集合的输出顺序不固定,上面的结果只是一个示例。

原本列表中有 6 个元素,其中包含重复的服务器名称;转换成集合后,只剩下 4 个不同的元素。

这就是 set 最直观的特点:自动去重,而且不保证元素的顺序。

创建 Set 集合

创建集合主要有两种方式:使用 set() 构造函数,或者使用花括号 {} 直接定义元素。

使用 set() 创建集合

先来看最简单的情况。

1
2
3
4
empty_set = set()

print(empty_set)
print(len(empty_set))

输出:

1
2
set()
0

这里需要注意一个容易踩坑的地方。

虽然 Python 使用 {} 表示集合字面量,但空的 {} 创建的是字典,而不是集合。

1
2
3
4
5
a = {}
b = set()

print(type(a))
print(type(b))

输出:

1
2
<class 'dict'>
<class 'set'>

所以,如果需要创建一个空集合,应该使用 set()。

set() 还可以接收列表、元组、字符串等可迭代对象,并将其中不重复的元素放入集合。

1
2
3
4
names = ["Tom", "Jerry", "Tom", "Alice"]
unique_names = set(names)

print(unique_names)

输出示例:

1
{'Jerry', 'Alice', 'Tom'}

如果传入的是字符串,Python 会把字符串中的每个字符当作一个独立元素。

1
2
3
4
letters = set("mississippi")

print(letters)
print(len(letters))

输出示例:

1
2
{'m', 'i', 's', 'p'}
4

字符串 mississippi 中虽然有很多重复字符,但集合只会保留不同的字符。

使用花括号创建集合

如果已经知道集合中有哪些元素,也可以直接使用花括号。

1
2
3
4
5
6
7
8
9
10
11
12
weekdays = {
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"
}

print(weekdays)
print(len(weekdays))

输出示例:

1
2
{'Sun', 'Mon', 'Sat', 'Tue', 'Fri', 'Wed', 'Thu'}
7

即使在代码中按照某个顺序填写元素,集合也不承诺按照这个顺序保存或输出。

因此,如果业务要求按照服务器名称、时间或者优先级排列数据,就不要依赖 set 的顺序。需要排序时,可以使用 sorted()。

1
2
3
servers = {"web-03", "web-01", "web-02"}

print(sorted(servers))

输出:

1
['web-01', 'web-02', 'web-03']

这里返回的是列表,而不是集合。

Set 中的元素有什么限制?

集合中的元素必须是可哈希的(hashable),因此常见的字符串、整数等可以作为集合元素,但列表和字典不能直接放进集合。

例如:

1
2
3
4
5
servers = {"web-01", "web-02"}
numbers = {10, 20, 30}

print(servers)
print(numbers)

输出

1
2
{'web-02', 'web-01'}
{10, 20, 30}

这些写法没有问题。

但是下面的代码会报错:

1
items = {[1, 2], [3, 4]}

因为列表是可变对象,不能直接作为集合元素。

1
2
3
4
5
Traceback (most recent call last):
File "<python-input-141>", line 1, in <module>
items = {[1, 2], [3, 4]}
^^^^^^^^^^^^^^^^
TypeError: cannot use 'list' as a set element (unhashable type: 'list')

如果确实需要把一组固定的值作为集合元素,可以考虑使用元组,前提是元组内部的所有元素也都可哈希。

1
2
3
items = {(1, 2), (3, 4)}

print(items)

输出示例:

1
{(1, 2), (3, 4)}

如何判断元素是否存在?

在 Linux 运维中,我们经常需要判断某台服务器是否已经出现在巡检清单中,或者某个服务是否属于预期的服务列表。

如果使用列表,可以通过 in 判断元素是否存在;集合同样支持这个操作。

1
2
3
4
5
servers = {"web-01", "web-02", "db-01"}

print("web-01" in servers)
print("web-03" in servers)
print("db-01" not in servers)

输出:

1
2
3
True
False
False

其中:

  • in:判断元素是否存在。
  • not in:判断元素是否不存在。

如果服务器名称存在,结果就是 True;否则就是 False。

那么,为什么不直接使用列表呢?

如果只是几台服务器,使用列表来判断某台服务器是否存在,完全没有问题。

但如果需要检查几千台甚至几万台服务器,并且反复判断某个服务器名称是否在清单中,使用集合通常会更高效。

这是因为,列表查找就像翻阅一本没有目录的名单,通常需要从头开始,一个一个地查找;而集合更像是利用哈希机制快速定位目标,通常不需要逐个比较所有元素。

因此,数据越多、查找越频繁,集合在查找效率上的优势就越明显。

Set 的常用方法

了解集合的创建和查找之后,再来看如何修改集合中的元素。

add():添加一个元素

如果需要向集合中添加一个元素,可以使用 add()。

1
2
3
4
5
6
servers = {"web-01", "web-02"}

servers.add("web-03")
servers.add("web-01")

print(servers)

输出示例:

1
{'web-02', 'web-01', 'web-03'}

这里连续调用了两次 add()。

第一次添加 web-03,因为它原本不存在,所以集合中增加了一个元素。

第二次添加 web-01,但它原本就存在,因此集合不会增加重复项。

这意味着,即使不提前判断元素是否存在,也可以直接使用 add() 完成去重添加。

update():批量添加元素

如果需要一次添加多个元素,可以使用 update()。

1
2
3
4
5
6
7
servers = {"web-01", "web-02"}

new_servers = ["web-02", "web-03", "db-01"]

servers.update(new_servers)

print(servers)

输出示例:

1
{'web-02', 'db-01', 'web-01', 'web-03'}

update() 会遍历传入的可迭代对象,将其中的元素逐个添加到集合中。

即使新数据里存在重复元素,或者与原集合中的元素重复,也不会产生重复项。

它还可以接收多个可迭代对象:

1
2
3
4
5
6
7
8
servers = {"web-01"}

servers.update(
["web-02", "web-03"],
("db-01", "db-02")
)

print(servers)

输出示例:

1
{'db-01', 'web-01', 'web-02', 'web-03', 'db-02'}

注意,update() 修改的是原集合,不会返回一个新的集合供你接收。

remove() 和 discard():删除元素

如果需要从集合中删除元素,可以使用 remove() 或 discard()。

两者看起来很像,但存在一个重要区别。

1
2
3
4
5
6
servers = {"web-01", "web-02", "db-01"}

servers.remove("web-02")
servers.discard("web-03")

print(servers)

输出示例:

1
{'db-01', 'web-01'}

这里,remove() 删除了存在的 web-02;discard() 尝试删除不存在的 web-03,但没有报错。

如果使用 remove() 删除一个不存在的元素,就会引发 KeyError。

1
2
3
servers = {"web-01", "web-02"}

servers.remove("web-03")

错误示例:

1
2
3
4
5
Traceback (most recent call last):
File "<python-input-152>", line 3, in <module>
servers.remove("web-03")
~~~~~~~~~~~~~~^^^^^^^^^^
KeyError: 'web-03'

而 discard() 不会因为元素不存在而报错。

因此,在自动化运维脚本中,如果无法确定元素是否存在,而且删除失败不需要触发异常,使用 discard() 往往更方便。

pop():删除并返回一个元素

集合还提供了 pop() 方法。

1
2
3
4
5
6
servers = {"web-01", "web-02", "db-01"}

server = servers.pop()

print("Deleted:", server)
print("Remaining:", servers)

输出示例:

1
2
Deleted: web-02
Remaining: {'db-01', 'web-01'}

这里的 web-02 只是示例,实际删除哪个元素取决于集合本身的状态,不能依赖某个固定顺序。

如果集合为空,调用 pop() 会引发 KeyError。

因此,pop() 不适合用来删除指定的服务器名称。如果目标是删除某个确定的元素,应当使用 remove() 或 discard()。

clear():清空集合

如果需要一次删除集合中的所有元素,可以使用 clear()。

1
2
3
4
5
6
servers = {"web-01", "web-02", "db-01"}

servers.clear()

print(servers)
print(len(servers))

输出:

1
2
set()
0

执行 clear() 后,原集合仍然存在,只是其中不再包含任何元素。

集合之间的关系:子集与超集

除了添加、删除和查找,集合还可以用来判断两个数据集合之间的包含关系。

例如,某次巡检计划需要检查以下服务器:

1
2
3
4
5
planned_servers = {
"web-01",
"web-02",
"db-01"
}

实际完成巡检的服务器是:

1
2
3
4
checked_servers = {
"web-01",
"web-02"
}

显然,实际完成巡检的服务器都属于计划清单,但计划清单中的服务器并没有全部完成检查。

这时可以使用 issubset() 和 issuperset()。

1
2
print(checked_servers.issubset(planned_servers))
print(planned_servers.issuperset(checked_servers))

输出:

1
2
True
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
2
web_servers = {"web-01", "web-02", "web-03"}
nginx_servers = {"web-02", "web-03", "web-04"}

web_servers 表示业务侧登记的 Web 服务器,nginx_servers 表示已经确认安装 Nginx 的服务器。

我们希望回答几个问题:

  • 两份清单合起来,一共有多少台不同的服务器?
  • 哪些服务器同时出现在两份清单中?
  • 哪些服务器只出现在第一份清单中?
  • 哪些服务器只出现在其中一份清单中?

这些问题都可以通过集合运算解决。

并集:合并两份清单

并集使用 | 运算符,也可以使用 union() 方法。

1
2
3
all_servers = web_servers | nginx_servers

print(all_servers)

输出示例:

1
{'web-02', 'web-01', 'web-04', 'web-03'}

并集会收集两个集合中的所有不同元素。

即使 web-02 和 web-03 同时出现在两份清单中,结果也只会保留一份。

使用方法调用也可以得到相同结果:

1
2
3
all_servers = web_servers.union(nginx_servers)

print(all_servers)

输出示例:

1
{'web-02', 'web-01', 'web-04', 'web-03'}

交集:找出共同元素

交集使用 & 运算符,也可以使用 intersection() 方法。

1
2
3
common_servers = web_servers & nginx_servers

print(common_servers)

输出:

1
{'web-02', 'web-03'}

交集只保留同时存在于两个集合中的元素。

在运维工作中,可以利用它找出两份清单中重复出现的服务器,或者核对某些主机是否同时满足两组条件。

差集:找出缺少的元素

差集使用 - 运算符,也可以使用 difference() 方法。

1
2
print(web_servers - nginx_servers)
print(nginx_servers - web_servers)

输出:

1
2
{'web-01'}
{'web-04'}

这里需要特别注意:差集与操作数的顺序有关。

web_servers - nginx_servers 表示存在于 web_servers,但不存在于 nginx_servers 中的元素。

反过来,nginx_servers - web_servers 则表示只存在于 nginx_servers 中的元素。

这在日常运维中非常实用。例如,想找出登记在业务清单中、但尚未确认安装 Nginx 的服务器,就可以通过第一种差集得到结果。

当然,这个判断依赖于两份清单的数据准确性;差集只能告诉我们数据之间的差异,并不能单凭结果证明服务器上实际运行了什么服务。

对称差集:找出双方不共有的元素

对称差集使用 ^ 运算符,也可以使用 symmetric_difference() 方法。

1
2
3
different_servers = web_servers ^ nginx_servers

print(different_servers)

输出:

1
{'web-01', 'web-04'}

对称差集会保留只出现在其中一个集合中的元素,而排除双方共有的元素。

可以把它理解为:先把两个集合合并,再去掉交集中的元素。

如果需要比较两份服务器清单,找出双方存在差异的部分,对称差集就很方便。

集合运算会修改原集合吗?

前面使用 |、&、- 和 ^ 得到的都是新集合,原来的集合不会因此改变。

1
2
3
4
5
6
7
8
a = {"web-01", "web-02"}
b = {"web-02", "web-03"}

c = a | b

print("a:", a)
print("b:", b)
print("c:", c)

输出示例:

1
2
3
a: {'web-02', 'web-01'}
b: {'web-02', 'web-03'}
c: {'web-02', 'web-01', 'web-03'}

如果希望直接修改原集合,也可以使用对应的增强赋值运算符。

1
2
3
4
5
6
7
a = {"web-01", "web-02"}
b = {"web-02", "web-03"}

a |= b

print(a)
print(b)

输出示例:

1
2
{'web-02', 'web-01', 'web-03'}
{'web-02', 'web-03'}

a |= b 相当于将并集结果更新到 a 中。类似地,还有 &=、-= 和 ^=,分别用于原地更新交集、差集和对称差集。

实战:利用 Set 对比服务器清单

前面分别学习了集合的创建、去重、成员查找和集合运算。现在把它们放到一个更接近 Linux 运维的场景中。

假设我们维护着一份业务服务器清单,同时还有一份已经完成 Nginx 检查的服务器清单。

我们希望自动完成以下工作:

  • 去除服务器清单中的重复名称。
  • 找出已经完成检查的服务器。
  • 找出尚未完成检查的服务器。
  • 找出出现在检查记录中、但不在业务清单里的服务器。
  • 计算检查覆盖率。

可以这样实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env python3

# 业务登记的服务器清单,可能存在重复项
planned_servers = [
"web-01",
"web-02",
"web-03",
"db-01",
"web-02"
]

# 已完成 Nginx 检查的服务器
checked_servers = {
"web-01",
"web-03",
"db-02"
}

# 将业务清单转换为集合,自动去重
planned_set = set(planned_servers)

# 已完成检查且属于业务清单的服务器
completed = planned_set & checked_servers

# 计划中存在,但尚未完成检查的服务器
pending = planned_set - checked_servers

# 检查记录中存在,但不在业务清单里的服务器
unexpected = checked_servers - planned_set

# 计算检查覆盖率
if planned_set:
coverage = len(completed) / len(planned_set) * 100
else:
coverage = 0

print("业务服务器总数:", len(planned_set))
print("已完成检查:", sorted(completed))
print("尚未完成检查:", sorted(pending))
print("清单外的检查记录:", sorted(unexpected))
print(f"检查覆盖率: {coverage:.2f}%")

输出:

1
2
3
4
5
业务服务器总数: 4
已完成检查: ['web-01', 'web-03']
尚未完成检查: ['db-01', 'web-02']
清单外的检查记录: ['db-02']
检查覆盖率: 50.00%

这个例子把集合的几种核心能力串联了起来。

业务清单中有 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 字典。它通过键值对组织数据,能够帮助我们更方便地保存服务器信息、读取配置,以及根据服务器名称快速查找对应的数据。