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 集合实战,搞定数据去重与集合运算
  15. Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
  16. Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
  17. Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
  18. Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
  19. Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
  20. Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
  21. Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
  22. Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
  23. Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
  24. Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
  25. Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
  26. Python 系列(二十六) Python 模块入门,从 import 到模块复用
  27. Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
  28. Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
  29. Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
  30. Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
  31. Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
  32. Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
  33. Python 系列(三十三) 什么是类和对象?零基础理解 Python class
  34. Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
  35. Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
  36. Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
  37. Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
  38. Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
  39. Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
  40. Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
  41. Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
  42. Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
  43. Python 系列(四十三) 别只会处理异常!学会自定义异常,让程序更容易维护
  44. Python 系列(四十四) 从零认识 assert 断言,让程序学会检查自己
  45. Python 系列(四十五) 使用 sys 模块处理标准输入与输出
  46. Python 系列(四十六) 使用 open() 读取和写入文件
  47. Python 系列(四十七) 学会读写文本文件,让程序保存运行结果
  48. Python 系列(四十八) 搞懂 bytes、bytearray 和二进制读写
  49. Python 系列(四十九) 使用 seek() 和 tell() 灵活控制文件读取位置
  50. Python 系列(五十) 掌握 os 和 os.path,实现文件与目录自动化管理
  51. Python 系列(五十一) 掌握列表推导式,轻松实现列表生成与数据筛选
  52. Python 系列(五十二) 掌握字典推导式,让字典创建更简单
  53. Python 系列(五十三) 从零理解生成器,轻松处理大量数据
  54. Python 系列(五十五) 使用 zip() 同时遍历多个集合

在前面的文章中,我们学习了列表、字典、循环以及列表推导式等知识。现在,我们已经可以把数据保存起来,并使用 for 循环逐个处理其中的元素。

但在实际工作中,有时我们需要同时处理两组甚至多组相关的数据。

例如,我们有一组服务器名称和一组 IP 地址:

1
2
servers = ["web01", "web02", "web03"]
ips = ["192.168.1.10", "192.168.1.11", "192.168.1.12"]

这两组数据存在对应关系:

  • web01 对应 192.168.1.10
  • web02 对应 192.168.1.11
  • web03 对应 192.168.1.12

如果我们希望同时输出服务器名称和 IP 地址,应该怎么做?

一种方法是通过下标分别获取两组数据中的元素,但这样写起来比较繁琐。

Python 提供了一个非常实用的内置函数:zip()。

它可以把多个可迭代对象中相同位置的元素组合在一起,让我们能够同时处理多组相关数据。

为什么需要 zip()

先看一个比较直观的例子。

假设我们有两份名单,一份记录服务器名称,另一份记录服务器 IP 地址。

1
2
3
4
5
servers = ["web01", "web02", "web03"]
ips = ["192.168.1.10", "192.168.1.11", "192.168.1.12"]

for i in range(len(servers)):
print(servers[i], ips[i])

这里出现了几个之前学过的知识点。

len(servers) 用于获取服务器列表的长度,结果是 3。

range(3) 会依次产生 0、1、2 这三个数字。

循环每执行一次,就使用相同的下标从两个列表中取出元素。

运行结果:

1
2
3
web01 192.168.1.10
web02 192.168.1.11
web03 192.168.1.12

这种写法可以正常工作,但需要自己管理下标。

如果有三组、四组数据需要同时处理,代码就会变得更加复杂。

使用 zip(),我们可以把这些工作交给 Python 完成。

认识 zip() 的基本用法

先来看最简单的例子:

1
2
3
4
5
6
numbers = [1, 2, 3]
letters = ["a", "b", "c"]

result = zip(numbers, letters)

print(list(result))

运行结果:

1
[(1, 'a'), (2, 'b'), (3, 'c')]

结果中出现了三个元组:

1
2
3
(1, 'a')
(2, 'b')
(3, 'c')

这里的元组可以理解为一个小组合,用来把同一位置的数据放在一起。

zip() 按照位置进行配对:

第一个列表第二个列表组合结果
1"a"(1, "a")
2"b"(2, "b")
3"c"(3, "c")

可以把这个过程理解为:第一组数据拿出第一个元素,第二组数据也拿出第一个元素,然后将它们组合起来;接着处理第二个位置、第三个位置,以此类推。

理解 zip() 的工作过程

flowchart TD
    A["第一个列表"] --> B["zip()"]
    C["第二个列表"] --> B
    B --> D["按位置组合元素"]
    D --> E["(1, a)"]
    D --> F["(2, b)"]
    D --> G["(3, c)"]

需要注意,zip() 本身返回的是一个迭代器,而不是直接返回一个列表。

对于刚接触 Python 的读者,可以先把迭代器理解为一种能够逐个提供数据的对象。它不会因为创建完成,就立即把所有结果都整理成一个列表。

因此,在上面的例子中:

1
result = zip(numbers, letters)

这行代码创建了一个 zip 对象。

而:

1
print(list(result))

则使用 list() 取出其中的所有组合,并将它们转换成列表,方便我们查看结果。

对于数据量较小的示例,使用 list(zip(...)) 可以让结果更加直观。

使用 zip() 同时遍历多个集合

理解基本用法之后,我们可以把 zip() 放进 for 循环中。

继续使用服务器名称和 IP 地址的例子:

1
2
3
4
5
servers = ["web01", "web02", "web03"]
ips = ["192.168.1.10", "192.168.1.11", "192.168.1.12"]

for server, ip in zip(servers, ips):
print(f"服务器:{server},IP 地址:{ip}")

运行结果:

1
2
3
服务器:web01,IP 地址:192.168.1.10
服务器:web02,IP 地址:192.168.1.11
服务器:web03,IP 地址:192.168.1.12

我们逐步理解这段代码。

首先:

1
zip(servers, ips)

将两个列表中相同位置的元素组合起来。

第一组数据是:

1
("web01", "192.168.1.10")

第二组数据是:

1
("web02", "192.168.1.11")

第三组数据是:

1
("web03", "192.168.1.12")

然后,for 循环每次取出一个组合。

1
for server, ip in zip(servers, ips):

这里的 server 和 ip 是我们自己定义的变量名称。

每次循环时,Python 会把组合中的第一个元素赋值给 server,第二个元素赋值给 ip。

例如,第一次循环时,相当于:

1
2
server = "web01"
ip = "192.168.1.10"

第二次循环时,则相当于:

1
2
server = "web02"
ip = "192.168.1.11"

因此,我们不需要手动编写下标,也不需要自己维护循环次数,就能同时遍历两个列表。

这种方式通常比通过 range() 和列表下标来配对数据更加简洁。

zip() 可以同时处理三组或更多数据吗?

当然可以。

只要这些数据之间存在相同位置的对应关系,就可以将多个可迭代对象交给 zip()。

例如,我们希望同时记录服务器名称、IP 地址和运行环境:

1
2
3
4
5
6
7
8
9
10
11
12
servers = ["web01", "web02", "web03"]

ips = [
"192.168.1.10",
"192.168.1.11",
"192.168.1.12"
]

environments = ["生产环境", "测试环境", "开发环境"]

for server, ip, environment in zip(servers, ips, environments):
print(f"服务器:{server},IP:{ip},环境:{environment}")

运行结果:

1
2
3
服务器:web01,IP:192.168.1.10,环境:生产环境
服务器:web02,IP:192.168.1.11,环境:测试环境
服务器:web03,IP:192.168.1.12,环境:开发环境

这一次,zip() 同时处理了三个列表。

每次循环都会取出三个相同位置的元素,并分别赋值给 server、ip 和 environment。

如果有四组、五组数据,也可以使用相同的方式。

不过,变量的数量必须与每次组合中包含的元素数量相对应。例如,三个列表组合后每次会得到三个元素,因此循环变量也应该有三个。

如果多个列表的长度不一样,会发生什么?

这是使用 zip() 时非常重要的一点。

假设服务器列表中有三个服务器,但 IP 地址列表中只有两个地址:

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

ips = [
"192.168.1.10",
"192.168.1.11"
]

print(list(zip(servers, ips)))

运行结果:

1
2
3
4
[
('web01', '192.168.1.10'),
('web02', '192.168.1.11')
]

你可能会发现,第三台服务器 web03 没有出现在结果中。

这是因为普通的 zip() 会在最短的可迭代对象耗尽时停止。

这里的“耗尽”,可以简单理解为:某个列表已经没有下一个元素可以提供了。

当 ips 中的两个地址都处理完成后,zip() 就会停止,不会继续单独处理 servers 中剩余的元素。

为什么需要特别注意这个问题?

在日常编程中,两个列表的长度不一样并不一定是错误。

例如,一组数据记录了所有服务器,另一组数据只记录了其中一部分服务器的 IP 地址。

但如果你认为两个列表中的每个元素都必须一一对应,那么长度不一致就可能导致数据被遗漏。

在运维场景中,如果服务器列表中有 100 台服务器,而 IP 地址列表只有 99 个地址,直接使用 zip() 就可能让最后一台服务器没有被处理。

因此,在处理要求严格一一对应的数据时,可以先检查长度:

1
2
3
4
5
6
7
8
servers = ["web01", "web02", "web03"]
ips = ["192.168.1.10", "192.168.1.11"]

if len(servers) != len(ips):
print("错误:服务器列表和 IP 地址列表长度不一致")
else:
for server, ip in zip(servers, ips):
print(f"{server}: {ip}")

运行结果:

1
错误:服务器列表和 IP 地址列表长度不一致

这里使用了之前学过的 if 条件判断。

只有当两个列表长度相同时,程序才会进入 else 分支进行配对处理。

这样就能避免因为数据数量不一致,而在不知情的情况下遗漏元素。

使用 zip_longest() 保留所有数据

如果我们希望即使某个列表提前结束,也能保留其他列表中剩余的数据,可以使用标准库 itertools 提供的 zip_longest()。

先看一个例子:

1
2
3
4
5
6
7
8
from itertools import zip_longest

servers = ["web01", "web02", "web03"]
ips = ["192.168.1.10", "192.168.1.11"]

result = list(zip_longest(servers, ips, fillvalue="未知"))

print(result)

运行结果:

1
2
3
4
5
[
('web01', '192.168.1.10'),
('web02', '192.168.1.11'),
('web03', '未知')
]

与普通的 zip() 不同,zip_longest() 会继续处理较长列表中剩余的元素。

其中:

1
fillvalue="未知"

表示当某个列表已经没有元素时,使用 "未知" 填补缺失的位置。

因此,第三台服务器仍然保留在结果中,只是它对应的 IP 地址被标记为 "未知"。

zip_longest() 需要通过 from itertools import zip_longest 导入后才能使用。

需要注意,填充值只是用来表示数据缺失,并不意味着程序真的找到了对应的 IP 地址。在处理真实业务数据时,应该根据需要进一步检查缺失信息。

zip() 与字典有什么关系?

前面我们学习过字典,可以使用键值对保存有关联的数据。

例如:

1
2
3
4
5
6
servers = ["web01", "web02", "web03"]
ips = ["192.168.1.10", "192.168.1.11", "192.168.1.12"]

server_ips = dict(zip(servers, ips))

print(server_ips)

运行结果:

1
2
3
4
5
{
'web01': '192.168.1.10',
'web02': '192.168.1.11',
'web03': '192.168.1.12'
}

这段代码将两组列表组合起来,再使用 dict() 转换为字典。

转换之后,服务器名称成为字典的键,IP 地址成为对应的值。

例如:

1
print(server_ips["web02"])

运行结果:

1
192.168.1.11

这样,我们就可以通过服务器名称快速查找对应的 IP 地址。

这里涉及三个知识点:

  • zip():将相同位置的元素组合起来。
  • dict():将符合键值对要求的数据转换为字典。
  • 字典的键访问:通过服务器名称获取对应的 IP 地址。

不过,如果两个列表中存在重复的服务器名称,转换成字典后,相同的键不会保留多份独立记录,后面的对应值会覆盖前面的值。因此,只有在确认键的唯一性符合业务要求时,才适合直接这样转换。

在 Linux 运维中使用 zip()

理解了基本语法之后,我们再来看一个更加贴近实际工作的例子。

假设我们需要维护一份服务器清单,分别记录服务器名称、IP 地址和 SSH 端口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
servers = ["web01", "web02", "db01"]

ips = [
"192.168.1.10",
"192.168.1.11",
"192.168.1.20"
]

ports = [22, 2222, 22]

for server, ip, port in zip(servers, ips, ports):
print(f"服务器:{server}")
print(f"IP 地址:{ip}")
print(f"SSH 端口:{port}")
print("-" * 30)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
服务器:web01
IP 地址:192.168.1.10
SSH 端口:22
------------------------------
服务器:web02
IP 地址:192.168.1.11
SSH 端口:2222
------------------------------
服务器:db01
IP 地址:192.168.1.20
SSH 端口:22
------------------------------

在这个例子中,我们使用三个列表保存不同类型的信息。

由于每个列表中相同位置的元素属于同一台服务器,因此可以使用 zip() 将它们组合起来。

这样,我们就不需要分别编写三个循环,也不需要手动使用下标取出每一组数据。

当然,这里只是演示如何整理服务器清单,并没有真正连接服务器或检查 SSH 服务。后续学习文件处理、网络请求等知识后,我们还可以在此基础上逐步扩展自动化运维脚本。

zip() 的惰性计算与迭代器特性

前面提到,zip() 返回的是一个迭代器。这一点值得单独理解。

例如:

1
2
3
4
5
6
numbers = [1, 2, 3]
letters = ["a", "b", "c"]

result = zip(numbers, letters)

print(result)

运行结果类似:

1
<zip object at 0x...>

你看到的不是组合后的列表,而是一个 zip 对象。对象显示的内存地址等信息可能因运行环境而异。

为什么不直接显示所有结果呢?

因为 zip() 采用惰性计算方式。简单来说,创建 zip 对象时,它不会立即把所有组合都转换成一个完整列表,而是在需要取出元素时逐步产生组合。

例如:

1
2
3
4
5
6
7
8
numbers = [1, 2, 3]
letters = ["a", "b", "c"]

result = zip(numbers, letters)

print(next(result))
print(next(result))
print(next(result))

运行结果:

1
2
3
(1, 'a')
(2, 'b')
(3, 'c')

next() 用于从迭代器中获取下一个元素。

第一次调用时,取出第一组组合;第二次调用时,取出第二组组合;第三次调用时,取出第三组组合。

如果继续调用 next(result),由于已经没有剩余组合,Python 就会抛出 StopIteration 异常。

日常编程中,我们通常使用 for 循环遍历迭代器,因为循环会在迭代结束时自动停止,不需要自己调用 next()。

还要注意,迭代器通常是一次性向前读取的。已经取出的元素不会自动回到开头,因此,如果需要再次遍历同一批结果,可以重新创建 zip() 对象,或者在数据量适合的情况下,先将结果转换为列表保存起来。

对于大型数据处理,惰性计算能够避免一次性创建完整结果列表所带来的额外内存开销。但如果传入的列表本身已经全部存放在内存中,那么这些原始列表占用的内存仍然存在。

总结

学习完本篇内容,我们应该掌握以下知识:

  • zip() 可以将多个可迭代对象中相同位置的元素组合起来。
  • 在 for 循环中使用 zip(),可以同时遍历多组数据。
  • 普通 zip() 会在最短的可迭代对象耗尽时停止。
  • 当需要保留所有数据时,可以考虑使用 itertools.zip_longest()。
  • dict(zip(keys, values)) 可以将两组对应数据组合成字典,但需要注意重复键的问题。
  • zip() 返回迭代器,采用惰性计算方式,通常可以直接使用 for 循环逐个处理结果。

动手练习

现在尝试自己完成下面的任务。

有三台服务器,它们的名称、IP 地址和 CPU 使用率分别保存在三个列表中:

1
2
3
servers = ["web01", "web02", "db01"]
ips = ["192.168.1.10", "192.168.1.11", "192.168.1.20"]
cpu_usage = [35, 82, 67]

请使用 zip() 同时遍历三个列表,并输出每台服务器的名称、IP 地址和 CPU 使用率。

输出效果如下:

1
2
3
服务器:web01,IP:192.168.1.10,CPU 使用率:35%
服务器:web02,IP:192.168.1.11,CPU 使用率:82%
服务器:db01,IP:192.168.1.20,CPU 使用率:67%

完成之后,还可以进一步尝试:当 CPU 使用率超过 80% 时,额外输出一条告警信息。

这道练习结合了列表、循环、zip() 和条件判断,能够帮助我们把前面学过的知识串联起来。

写在最后

zip() 看起来只是一个简单的内置函数,却能够帮助我们更加简洁地处理多组存在对应关系的数据。在服务器清单整理、批量数据处理以及自动化运维脚本中,都可能用到这种写法。

下一篇,我们将学习如何使用 Python 的模式匹配功能,查找、修改或拆分字符串,让程序能够更加灵活地处理文本内容。