在日常写 Python 的过程中,我们经常会遇到这样一类场景:手上有两份一一对应的列表,一份存原始数据,另一份是基于原始数据计算衍生出来的结果列表。我们需要同时迭代两份列表,把两两配对的数据拿出来做业务判断。
很多新手第一反应会用下标索引去循环取值,但这种写法暗藏可读性隐患,今天我们就聊聊 zip 这个内置工具,以及它容易踩的长度不一致的坑,还有兜底方案 itertools.zip_longest。
Bilibili 同步视频
📌 先看业务场景
举个简单例子:我们有一个名字列表 names,同时派生出来一个保存每个名字字符长度的列表 counts。
python
names = ['Cecilia', 'Lise', 'Marie']
counts = [len(n) for n in names]
print(counts)
# 输出:[7, 4, 5]
现在需求来了:找出列表当中名字字符最长的那一个名字。
❌ 写法 1:通过下标 range + 索引访问(糟糕写法)
很多初学者会写出下面这样的代码,依靠下标 i 分别去取两个列表的元素:
python
names = ['Cecilia', 'Lise', 'Marie']
counts = [len(n) for n in names]
longest_name = None
max_count = 0
for i in range(len(names)):
count = counts[i]
if count > max_count:
longest_name = names[i]
max_count = count
print(longest_name)
# 输出:Cecilia
✍️ 问题在哪? 循环变量
i仅仅作为下标工具,在循环体内要出现两次,分别索引names和counts。代码逻辑被下标干扰,阅读起来很绕。如果列表一多,下标反复书写,还很容易写错索引,埋下 bug。
有同学会说,那我用 enumerate 优化一下?
⚠️ 写法 2:enumerate 优化,但依旧不够优雅
enumerate 可以同时拿到下标和原列表元素,是 Python 很常用的工具,但面对两个并行列表,它治标不治本:
python
longest_name = None
max_count = 0
for i, name in enumerate(names):
count = counts[i]
if count > max_count:
longest_name = name
max_count = count
print(longest_name)
可以看到,我们依旧需要拿着下标 i 去访问第二个列表 counts。下标索引没有被彻底消除,代码依旧有冗余,当同时要遍历 3 个、4 个列表的时候,这种写法会变得更加臃肿。
✅ 优雅方案:内置 zip 函数登场
Python 内置的 zip() 函数可以把两个或多个迭代器封装成惰性生成器✨。
什么叫惰性生成器? 它不会一次性把全部配对结果都在内存生成出来,每一轮循环,才会分别从每一个输入迭代器取出下一个元素,打包成元组返回。哪怕你的列表百万级长度,也不会一次性吃光内存,性能表现很不错。
我们直接用 zip 改写上面的逻辑:
python
names = ['Cecilia', 'Lise', 'Marie']
counts = [len(n) for n in names]
longest_name = None
max_count = 0
# 直接解包配对后的元组,不再需要下标
for name, count in zip(names, counts):
if count > max_count:
longest_name = name
max_count = count
print(longest_name)
# 输出:Cecilia
哇,清爽太多!不再有恼人的下标变量,业务逻辑一目了然,循环体里只关心我们真正要用的 name 和 count。
⚠️ 重点避坑:zip 的截断特性!
zip 好用,但藏着一个极易踩的隐形陷阱:当传入的多个迭代器长度不一样时,zip 会以最短的那个迭代器为准,提前终止循环。长序列多出来的元素会直接被无声丢弃,不会抛任何报错!
来看演示案例:我们给 names 追加一个新名字 Rosalind,但是忘记同步更新 counts 列表。
python
names = ['Cecilia', 'Lise', 'Marie']
counts = [len(n) for n in names]
names.append('Rosalind')
for name, count in zip(names, counts):
print(name)
输出结果:
Plain
Cecilia
Lise
Marie
你会发现新增的 Rosalind 完全没有被打印出来!程序没有报错,悄咪咪丢掉了数据。这种静默失败的问题,调试的时候非常头疼。
💡什么时候不会踩坑? 当你的各个列表是同源推导出来的,保证长度完全一致,zip 可以放心大胆用。一旦无法保证序列长度相等,就不能直接使用 zip。
🛠️ 兜底方案:itertools.zip_longest
如果希望循环走到最长序列结束 ,不要丢掉任何一条数据,就需要使用标准库 itertools 中的 zip_longest。
当某个短迭代器提前耗尽,它不会终止循环,而是使用填充值补齐缺失的那一部分,默认填充值是 None,也可以通过 fillvalue 参数自定义填充内容。
python
import itertools
names = ['Cecilia', 'Lise', 'Marie', 'Rosalind']
counts = [7, 4, 5]
for name, count in itertools.zip_longest(names, counts):
print(f"{name}: {count}")
输出:
Plain
Cecilia: 7
Lise: 4
Marie: 5
Rosalind: None
我们也可以自定义填充值,比如缺失的时候填充 0:
python
for name, count in itertools.zip_longest(names, counts, fillvalue=0):
print(f"{name}: {count}")
输出:
Plain
Cecilia: 7
Lise: 4
Marie: 5
Rosalind: 0
📝 核心要点总结
-
内置
zip()可以一次性对多个迭代器做配对遍历,告别下标索引,代码可读性大幅提升。 -
zip返回惰性生成器,按需产出元组,处理超大序列时内存友好,不会一次性加载全部结果。 -
⚠️高危特性:
zip在任意一个输入迭代器遍历完成后就会停止,会截断长序列多余元素,并且不会抛出异常。 -
如果需要遍历至最长迭代器结束 ,请选用
itertools.zip_longest,可以通过fillvalue指定空缺位置的填充数据。
💡小建议:写代码的时候要分清场景:
确认所有待遍历列表长度完全一致 → 直接用
zip无法保证等长,又不想丢失数据 → 使用
zip_longest

这篇文章讲完了 zip 的使用与坑点,这个需求挺适合做成可预览网页,工作任务模式处理页面和交互会更完整,要不要用它继续?