Python 系列(五十五) 使用 zip() 同时遍历多个集合
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 集合实战,搞定数据去重与集合运算
- Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
- Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
- Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
- Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
- Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
- Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
- Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
- Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
- Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
- Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
- Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
- Python 系列(二十六) Python 模块入门,从 import 到模块复用
- Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
- Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
- Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
- Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
- Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
- Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
- Python 系列(三十三) 什么是类和对象?零基础理解 Python class
- Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
- Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
- Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
- Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
- Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
- Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
- Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
- Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常
- Python 系列(四十三) 别只会处理异常!学会自定义异常,让程序更容易维护
- Python 系列(四十四) 从零认识 assert 断言,让程序学会检查自己
- Python 系列(四十五) 使用 sys 模块处理标准输入与输出
- Python 系列(四十六) 使用 open() 读取和写入文件
- Python 系列(四十七) 学会读写文本文件,让程序保存运行结果
- Python 系列(四十八) 搞懂 bytes、bytearray 和二进制读写
- Python 系列(四十九) 使用 seek() 和 tell() 灵活控制文件读取位置
- Python 系列(五十) 掌握 os 和 os.path,实现文件与目录自动化管理
- Python 系列(五十一) 掌握列表推导式,轻松实现列表生成与数据筛选
- Python 系列(五十二) 掌握字典推导式,让字典创建更简单
- Python 系列(五十三) 从零理解生成器,轻松处理大量数据
- Python 系列(五十五) 使用 zip() 同时遍历多个集合
在前面的文章中,我们学习了列表、字典、循环以及列表推导式等知识。现在,我们已经可以把数据保存起来,并使用 for 循环逐个处理其中的元素。
但在实际工作中,有时我们需要同时处理两组甚至多组相关的数据。
例如,我们有一组服务器名称和一组 IP 地址:
1 | servers = ["web01", "web02", "web03"] |
这两组数据存在对应关系:
web01对应192.168.1.10web02对应192.168.1.11web03对应192.168.1.12
如果我们希望同时输出服务器名称和 IP 地址,应该怎么做?
一种方法是通过下标分别获取两组数据中的元素,但这样写起来比较繁琐。
Python 提供了一个非常实用的内置函数:zip()。
它可以把多个可迭代对象中相同位置的元素组合在一起,让我们能够同时处理多组相关数据。
为什么需要 zip()
先看一个比较直观的例子。
假设我们有两份名单,一份记录服务器名称,另一份记录服务器 IP 地址。
1 | servers = ["web01", "web02", "web03"] |
这里出现了几个之前学过的知识点。
len(servers) 用于获取服务器列表的长度,结果是 3。
range(3) 会依次产生 0、1、2 这三个数字。
循环每执行一次,就使用相同的下标从两个列表中取出元素。
运行结果:
1 | web01 192.168.1.10 |
这种写法可以正常工作,但需要自己管理下标。
如果有三组、四组数据需要同时处理,代码就会变得更加复杂。
使用 zip(),我们可以把这些工作交给 Python 完成。
认识 zip() 的基本用法
先来看最简单的例子:
1 | numbers = [1, 2, 3] |
运行结果:
1 | [(1, 'a'), (2, 'b'), (3, 'c')] |
结果中出现了三个元组:
1 | (1, 'a') |
这里的元组可以理解为一个小组合,用来把同一位置的数据放在一起。
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 | servers = ["web01", "web02", "web03"] |
运行结果:
1 | 服务器:web01,IP 地址:192.168.1.10 |
我们逐步理解这段代码。
首先:
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 | server = "web01" |
第二次循环时,则相当于:
1 | server = "web02" |
因此,我们不需要手动编写下标,也不需要自己维护循环次数,就能同时遍历两个列表。
这种方式通常比通过 range() 和列表下标来配对数据更加简洁。
zip() 可以同时处理三组或更多数据吗?
当然可以。
只要这些数据之间存在相同位置的对应关系,就可以将多个可迭代对象交给 zip()。
例如,我们希望同时记录服务器名称、IP 地址和运行环境:
1 | servers = ["web01", "web02", "web03"] |
运行结果:
1 | 服务器:web01,IP:192.168.1.10,环境:生产环境 |
这一次,zip() 同时处理了三个列表。
每次循环都会取出三个相同位置的元素,并分别赋值给 server、ip 和 environment。
如果有四组、五组数据,也可以使用相同的方式。
不过,变量的数量必须与每次组合中包含的元素数量相对应。例如,三个列表组合后每次会得到三个元素,因此循环变量也应该有三个。
如果多个列表的长度不一样,会发生什么?
这是使用 zip() 时非常重要的一点。
假设服务器列表中有三个服务器,但 IP 地址列表中只有两个地址:
1 | servers = ["web01", "web02", "web03"] |
运行结果:
1 | [ |
你可能会发现,第三台服务器 web03 没有出现在结果中。
这是因为普通的 zip() 会在最短的可迭代对象耗尽时停止。
这里的“耗尽”,可以简单理解为:某个列表已经没有下一个元素可以提供了。
当 ips 中的两个地址都处理完成后,zip() 就会停止,不会继续单独处理 servers 中剩余的元素。
为什么需要特别注意这个问题?
在日常编程中,两个列表的长度不一样并不一定是错误。
例如,一组数据记录了所有服务器,另一组数据只记录了其中一部分服务器的 IP 地址。
但如果你认为两个列表中的每个元素都必须一一对应,那么长度不一致就可能导致数据被遗漏。
在运维场景中,如果服务器列表中有 100 台服务器,而 IP 地址列表只有 99 个地址,直接使用 zip() 就可能让最后一台服务器没有被处理。
因此,在处理要求严格一一对应的数据时,可以先检查长度:
1 | servers = ["web01", "web02", "web03"] |
运行结果:
1 | 错误:服务器列表和 IP 地址列表长度不一致 |
这里使用了之前学过的 if 条件判断。
只有当两个列表长度相同时,程序才会进入 else 分支进行配对处理。
这样就能避免因为数据数量不一致,而在不知情的情况下遗漏元素。
使用 zip_longest() 保留所有数据
如果我们希望即使某个列表提前结束,也能保留其他列表中剩余的数据,可以使用标准库 itertools 提供的 zip_longest()。
先看一个例子:
1 | from itertools import zip_longest |
运行结果:
1 | [ |
与普通的 zip() 不同,zip_longest() 会继续处理较长列表中剩余的元素。
其中:
1 | fillvalue="未知" |
表示当某个列表已经没有元素时,使用 "未知" 填补缺失的位置。
因此,第三台服务器仍然保留在结果中,只是它对应的 IP 地址被标记为 "未知"。
zip_longest() 需要通过 from itertools import zip_longest 导入后才能使用。
需要注意,填充值只是用来表示数据缺失,并不意味着程序真的找到了对应的 IP 地址。在处理真实业务数据时,应该根据需要进一步检查缺失信息。
zip() 与字典有什么关系?
前面我们学习过字典,可以使用键值对保存有关联的数据。
例如:
1 | servers = ["web01", "web02", "web03"] |
运行结果:
1 | { |
这段代码将两组列表组合起来,再使用 dict() 转换为字典。
转换之后,服务器名称成为字典的键,IP 地址成为对应的值。
例如:
1 | print(server_ips["web02"]) |
运行结果:
1 | 192.168.1.11 |
这样,我们就可以通过服务器名称快速查找对应的 IP 地址。
这里涉及三个知识点:
zip():将相同位置的元素组合起来。dict():将符合键值对要求的数据转换为字典。- 字典的键访问:通过服务器名称获取对应的 IP 地址。
不过,如果两个列表中存在重复的服务器名称,转换成字典后,相同的键不会保留多份独立记录,后面的对应值会覆盖前面的值。因此,只有在确认键的唯一性符合业务要求时,才适合直接这样转换。
在 Linux 运维中使用 zip()
理解了基本语法之后,我们再来看一个更加贴近实际工作的例子。
假设我们需要维护一份服务器清单,分别记录服务器名称、IP 地址和 SSH 端口。
1 | servers = ["web01", "web02", "db01"] |
运行结果:
1 | 服务器:web01 |
在这个例子中,我们使用三个列表保存不同类型的信息。
由于每个列表中相同位置的元素属于同一台服务器,因此可以使用 zip() 将它们组合起来。
这样,我们就不需要分别编写三个循环,也不需要手动使用下标取出每一组数据。
当然,这里只是演示如何整理服务器清单,并没有真正连接服务器或检查 SSH 服务。后续学习文件处理、网络请求等知识后,我们还可以在此基础上逐步扩展自动化运维脚本。
zip() 的惰性计算与迭代器特性
前面提到,zip() 返回的是一个迭代器。这一点值得单独理解。
例如:
1 | numbers = [1, 2, 3] |
运行结果类似:
1 | <zip object at 0x...> |
你看到的不是组合后的列表,而是一个 zip 对象。对象显示的内存地址等信息可能因运行环境而异。
为什么不直接显示所有结果呢?
因为 zip() 采用惰性计算方式。简单来说,创建 zip 对象时,它不会立即把所有组合都转换成一个完整列表,而是在需要取出元素时逐步产生组合。
例如:
1 | numbers = [1, 2, 3] |
运行结果:
1 | (1, 'a') |
next() 用于从迭代器中获取下一个元素。
第一次调用时,取出第一组组合;第二次调用时,取出第二组组合;第三次调用时,取出第三组组合。
如果继续调用 next(result),由于已经没有剩余组合,Python 就会抛出 StopIteration 异常。
日常编程中,我们通常使用 for 循环遍历迭代器,因为循环会在迭代结束时自动停止,不需要自己调用 next()。
还要注意,迭代器通常是一次性向前读取的。已经取出的元素不会自动回到开头,因此,如果需要再次遍历同一批结果,可以重新创建 zip() 对象,或者在数据量适合的情况下,先将结果转换为列表保存起来。
对于大型数据处理,惰性计算能够避免一次性创建完整结果列表所带来的额外内存开销。但如果传入的列表本身已经全部存放在内存中,那么这些原始列表占用的内存仍然存在。
总结
学习完本篇内容,我们应该掌握以下知识:
zip()可以将多个可迭代对象中相同位置的元素组合起来。- 在
for循环中使用zip(),可以同时遍历多组数据。 - 普通
zip()会在最短的可迭代对象耗尽时停止。 - 当需要保留所有数据时,可以考虑使用
itertools.zip_longest()。 dict(zip(keys, values))可以将两组对应数据组合成字典,但需要注意重复键的问题。zip()返回迭代器,采用惰性计算方式,通常可以直接使用for循环逐个处理结果。
动手练习
现在尝试自己完成下面的任务。
有三台服务器,它们的名称、IP 地址和 CPU 使用率分别保存在三个列表中:
1 | servers = ["web01", "web02", "db01"] |
请使用 zip() 同时遍历三个列表,并输出每台服务器的名称、IP 地址和 CPU 使用率。
输出效果如下:
1 | 服务器:web01,IP:192.168.1.10,CPU 使用率:35% |
完成之后,还可以进一步尝试:当 CPU 使用率超过 80% 时,额外输出一条告警信息。
这道练习结合了列表、循环、zip() 和条件判断,能够帮助我们把前面学过的知识串联起来。
写在最后
zip() 看起来只是一个简单的内置函数,却能够帮助我们更加简洁地处理多组存在对应关系的数据。在服务器清单整理、批量数据处理以及自动化运维脚本中,都可能用到这种写法。
下一篇,我们将学习如何使用 Python 的模式匹配功能,查找、修改或拆分字符串,让程序能够更加灵活地处理文本内容。
