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 列表实战,搞定数据增删改查与批量处理

假设你是一名 Linux 运维工程师,每天都需要检查公司的服务器运行状态。

刚开始,服务器只有三五台,你可以手动登录服务器,执行 uptime、free -h 和 df -h,看看系统负载、内存和磁盘空间是否正常。

后来,服务器越来越多,从几台变成几十台,甚至上百台。每天手动检查显然不太现实,于是你决定写一个 Python 脚本,把这些重复工作自动化。

问题来了:程序怎么知道需要检查哪些服务器?每台服务器的 IP 地址存在哪里?如果服务器列表里出现了重复的主机名,该怎么处理?如果一组检查项目是固定的,又该如何保存?

你可能会想到,直接把这些信息写在程序里不就行了吗?

当然可以,但随着需求越来越复杂,你会发现,不同的数据其实有不同的特点。

有些数据需要按照指定顺序依次处理,有些数据不能重复,有些数据需要通过服务器名称找到对应的 IP 地址,还有些数据在定义之后就不希望被随意修改。

如果全部使用同一种方式存储,处理起来就不那么方便了。

这就像整理日常运维工作中的各种信息:服务器清单、IP 地址表、巡检项目,各有各的用途,自然也需要不同的组织方式。

Python 已经为我们准备好了几种常用的数据组织工具。它们分别是 list、tuple、set 和 dict。

本篇我们先不急着深入具体操作,而是通过服务器巡检这个场景,认识这四种类型,了解它们各自适合解决什么问题,以及在编写 Python 程序时应该如何选择。

先从服务器清单说起:为什么需要不同的数据类型?

假设我们需要检查以下四台服务器:

  • web01
  • web02
  • db01
  • web01

等等,为什么 web01 出现了两次?

这可能是录入时不小心重复添加了,也可能是不同的配置文件合并之后产生了重复数据。无论是什么原因,我们都需要根据实际需求决定如何处理。

再往下想,巡检脚本还需要知道每台服务器的 IP 地址。例如,web01 对应 192.168.1.10,web02 对应 192.168.1.11。

如果只是保存一组服务器名称,和保存服务器名称与 IP 地址之间的对应关系,显然不是同一回事。

此外,我们可能还有一组固定的巡检项目,例如 CPU、内存和磁盘。它们需要按照一定的顺序执行,但在当前程序设计中并不需要随意增删。

到这里,我们已经遇到了四种不同的数据组织需求:

  1. 保存有顺序、可能重复的服务器名称。
  2. 保存一组不允许重复的服务器名称。
  3. 根据服务器名称找到对应的 IP 地址。
  4. 保存一组结构固定的巡检项目。

Python 的四大内置集合类型,恰好可以帮助我们处理这些问题。

认识 Python 的四大内置集合类型

List:保存需要依次处理的服务器清单

先解决第一个问题:如何保存需要检查的服务器名称?

我们可以使用 Python 的列表,也就是 list。

1
2
3
servers = ["web01", "web02", "db01", "web01"]

print(servers)

运行结果:

1
['web01', 'web02', 'db01', 'web01']

可以看到,四个元素都被保存了下来,包括重复出现的 web01。

列表有两个很重要的特点:

  • 有序: 元素按照列表中的位置排列,可以通过索引访问,也可以使用切片获取一部分元素。
  • 可变: 程序运行过程中,可以添加、删除或修改元素。

例如,巡检过程中又增加了一台缓存服务器:

1
2
3
servers.append("cache01")

print(servers)

此时,cache01 就被添加到了列表末尾。

运行结果:

1
['web01', 'web02', 'db01', 'web01', 'cache01']

在实际运维工作中,我们可以使用列表保存待巡检服务器、待处理日志文件、需要执行的任务,以及其他需要按照一定顺序处理的数据。

记住:如果需要保存一组有顺序、允许重复、后续可能增删改的数据,可以优先考虑 list。

下一篇博客,我们就会专门学习列表的创建、索引、切片和动态修改等操作。

Tuple:保存结构固定的巡检信息

再来看另一个需求。

假设当前巡检脚本固定检查三个项目:

  • CPU 使用情况
  • 内存使用情况
  • 磁盘空间

如果这些项目在当前设计中不需要直接增加、删除或替换,我们可以考虑使用元组,也就是 tuple。

1
2
3
check_items = ("cpu", "memory", "disk")

print(check_items)

运行结果:

1
('cpu', 'memory', 'disk')

乍一看,元组和列表很像,都是用来保存多个元素的,而且都可以通过索引访问元素。

它们的关键区别在于可变性。

列表创建之后,可以通过相应操作修改其中的元素;元组则不能修改自身的元素引用,也不能直接添加或删除元素。

例如,下面的操作会引发 TypeError:

1
2
3
check_items = ("cpu", "memory", "disk")

check_items[0] = "load"

报错如下:

1
2
3
4
5
Traceback (most recent call last):
File "<python-input-3>", line 3, in <module>
check_items[0] = "load"
~~~~~~~~~~~^^^
TypeError: 'tuple' object does not support item assignment

因为元组不支持通过索引替换元素。

需要注意,元组的不可变性并不意味着它内部引用的所有对象都不可变。如果元组中包含一个列表,该列表内部的元素仍然可以修改。

另外,元组通常比保存相同元素的列表占用更少的内存,但并不能笼统地认为元组在所有操作中都更快。

记住:如果需要保存有顺序、结构相对固定的数据,可以考虑 tuple。

后续其他文章我们将进一步学习元组的创建、访问、解包,以及它与列表的区别。

Set:处理重复出现的服务器名称

回到最开始的服务器清单:

1
servers = ["web01", "web02", "db01", "web01"]

如果我们的目标是获得不重复的服务器名称,该怎么办?

当然可以编写循环,逐个检查服务器名称是否已经出现过。不过,Python 已经提供了一种更直接的数据类型:集合,也就是 set。

1
2
3
4
5
servers = ["web01", "web02", "db01", "web01"]

unique_servers = set(servers)

print(unique_servers)

运行结果可能类似于:

1
{'web01', 'web02', 'db01'}

重复的 web01 只保留了一份。

这里发生了什么?

set() 将列表转换为集合。由于集合中的元素不重复,重复的服务器名称就被合并掉了。

不过,这里有一个容易忽略的细节:集合不保证元素的迭代顺序,因此不能依赖它按照原始列表的顺序输出数据。

如果后续巡检任务必须按照指定顺序执行,就不能简单地把集合当成原列表的替代品。

除了去重,集合还特别适合比较两组数据。

例如,昨天巡检的服务器和今天巡检的服务器并不完全相同,我们可能需要找出新增的服务器,或者发现哪些服务器已经不在当前清单中。

这时,集合的交集、并集和差集等运算就能派上用场。

记住:如果需要去重、检查成员关系,或者比较两组数据,可以考虑 set。

后续我们将在其他文章中深入学习集合的创建、增删、成员测试,以及交集、并集和差集等操作。

Dict:根据服务器名称查找 IP 地址

现在,服务器清单已经有了,但巡检脚本还需要知道每台服务器的 IP 地址。

例如:

  • web01 对应 192.168.1.10
  • web02 对应 192.168.1.11
  • db01 对应 192.168.1.20

如果我们仍然使用一个列表保存所有名称,另一个列表保存所有 IP 地址,那么就必须确保两个列表中的元素始终一一对应。随着数据增多,维护起来很容易出错。

有没有更合适的方式?

这时候就可以使用字典,也就是 dict。

1
2
3
4
5
server_ips = {
"web01": "192.168.1.10",
"web02": "192.168.1.11",
"db01": "192.168.1.20"
}

字典使用键(key)和值(value)之间的映射关系来组织数据。

在这个例子中,服务器名称是键,IP 地址是值。

如果需要查找 db01 的 IP 地址,可以直接通过键访问:

1
print(server_ips["db01"])

运行结果:

1
192.168.1.20

这样,我们就不需要手动遍历一组 IP 地址,再判断它属于哪台服务器了。

字典的键必须唯一,并且必须是可哈希的对象。字符串和整数通常可以作为键,而列表不能直接作为字典的键。

字典的值则可以重复,也可以保存不同类型的数据。

从 Python 3.7 开始,字典按照插入顺序迭代属于语言规范保证的行为。

在实际运维工作中,字典的用途非常广泛。例如:

  • 根据主机名查找 IP 地址。
  • 保存服务器的 CPU、内存和磁盘使用率。
  • 保存应用程序的配置参数。
  • 处理云平台 API 或 Kubernetes API 返回的结构化数据。

记住:如果需要通过一个标识查找对应的信息,可以优先考虑 dict。

我们在后续的文章中将深入学习字典的创建、查询、更新、删除和遍历等操作。

四大内置集合类型有什么区别?

经过前面的例子,我们已经认识了四种类型。

现在把它们放在一起,就更容易看出区别了。

特性listtuplesetdict
中文名称列表元组集合字典
主要用途保存有序数据保存固定结构的数据去重、集合运算键值映射
是否保证顺序是是否是,按插入顺序
能否修改容器可以不可以可以可以
是否允许重复允许允许元素不重复键不重复
典型访问方式索引、切片索引、切片成员测试通过键查找
运维场景服务器巡检清单固定巡检项目不重复的主机集合主机名与 IP 的映射

这里还需要明确几个细节。

第一,列表和元组都属于序列类型。

它们可以通过整数索引访问元素,也支持切片操作。

第二,集合不保证迭代顺序。

如果业务需要保留输入顺序,就不能依赖集合的输出顺序。去重时应该根据具体需求选择合适的处理方式。

第三,字典的键必须唯一且可哈希。

字典可以通过键查找对应的值,但键并不是任意对象都能充当的。

第四,不可变并不代表一定更高效。

元组在某些情况下具有内存优势,但选择数据结构时,应优先考虑数据的特点和程序的实际需求。

实际编写程序时,应该如何选择?

看到这里,你可能会问:既然四种类型都可以保存数据,实际写程序时到底该选哪个?

其实不用一开始就记住所有规则。只要先想清楚自己需要解决什么问题,通常就能找到合适的类型。

场景一:我需要保存一组服务器,并按顺序巡检

选择 list。

1
servers = ["web01", "web02", "db01"]

列表能够保留服务器的排列顺序,也允许后续添加或删除服务器。

场景二:我需要保存一组固定的参数

可以考虑 tuple。

1
server_endpoint = ("192.168.1.10", 8080)

这里用元组保存服务器地址和端口。假设程序设计要求这两个字段在创建之后不通过元组本身修改,那么元组就是一种合适的选择。

场景三:我只关心有哪些不同的服务器

选择 set。

1
servers = {"web01", "web02", "db01"}

集合适合去重、判断某台服务器是否属于某个集合,以及比较不同来源的服务器清单。

场景四:我需要根据主机名查找 IP 地址

选择 dict。

1
2
3
4
5
6
7
server_ips = {
"web01": "192.168.1.10",
"web02": "192.168.1.11",
"db01": "192.168.1.20"
}

print(server_ips["web01"])

字典可以根据键快速找到对应的值,而列表通常需要从头开始,一个一个地检查元素。

例如,服务器只有 10 台时,在列表中查找某台服务器并不麻烦。但如果服务器增加到 1000 台,甚至 1 万台,就可能需要检查更多的元素才能找到目标。

字典和集合则不同。它们通常能够更快地定位目标,不需要像列表那样逐个检查。因此,当需要频繁查找数据时,使用字典或集合往往更加合适。

当然,这并不意味着字典和集合在任何情况下都更快。具体选择哪种数据类型,还要看我们需要存储什么数据,以及后续要如何使用这些数据。

归纳起来,可以先记住下面四条:

  • 有序、可变: 优先考虑 list。
  • 有序、容器自身不可变: 考虑 tuple。
  • 元素不重复、集合运算: 优先考虑 set。
  • 键值映射、按键查找: 优先考虑 dict。

当然,这些只是入门阶段的选型原则。随着学习深入,你还会接触更多数据结构,以及不同操作对程序性能的影响。

如何创建这些集合类型?

除了直接使用 []、()、{} 等语法,Python 还提供了对应的内置构造函数:

  • list()
  • tuple()
  • set()
  • dict()

它们可以用于创建对应类型的对象,也可以在符合条件时将其他数据转换为相应类型。

例如:

1
2
3
4
5
6
7
servers = ["web01", "web02", "web01"]

server_tuple = tuple(servers)
server_set = set(servers)

print(server_tuple)
print(server_set)

输出:

1
2
('web01', 'web02', 'web01')
{'web01', 'web02'}

这里,tuple(servers) 将列表转换为元组,保留原有元素及其顺序;set(servers) 则将列表转换为集合,去除重复元素,但不保证原有顺序。

还有一个容易踩的坑:创建空集合时,需要使用 set()。

1
2
empty_set = set()
empty_dict = {}

为什么不使用 {} 创建空集合?

因为在 Python 中,空花括号 {} 创建的是空字典,而不是空集合。

这个细节看起来很小,但在刚接触 Python 时很容易弄混。

至于列表、元组、集合和字典还有哪些创建方式,以及不同操作的具体用法,我们会在后续文章中逐一展开。

写在最后

回到最开始的服务器巡检场景。

随着自动化需求不断增加,我们需要保存服务器清单、去除重复的主机名、查找 IP 地址,还需要组织固定的巡检项目。

这些需求看起来很普通,却对应着不同的数据组织方式。

Python 提供的 list、tuple、set 和 dict,正是为了帮助我们更方便地处理这些问题。

本篇我们先建立了整体认知,了解四种类型的特点、区别和适用场景。至于如何创建列表、怎样使用索引和切片、如何进行集合运算,以及字典为什么适合通过键查找数据,还需要通过后续的学习逐步掌握。

接下来,我们将从最常见的 list 入手,学习如何创建列表、访问元素、使用切片,以及在程序运行过程中动态修改列表。

掌握这些基础知识之后,再逐步学习 tuple、set 和 dict,最后通过综合示例,把它们应用到 Linux 运维脚本和实际自动化任务中。

学会一门编程语言,不只是记住语法,更重要的是知道面对实际问题时,应该选择什么工具,以及为什么这样选择。