Python 的 enumerate 和 zip 怎么用?为什么不要用 range(len(x)) 遍历?
简化版
**enumerate(iterable) 在遍历时同时给出「索引和元素」,zip(a, b, ...) 把多个可迭代对象「按位置配对」一起遍历——它们是 Python 遍历的两大惯用工具,能替代笨拙的 range(len(x)) 索引遍历。**① enumerate——for i, x in enumerate(seq) 直接拿到下标 i 和值 x,还能指定起始值 enumerate(seq, start=1)(从 1 开始编号);② zip——for a, b in zip(list1, list2) 同时遍历多个序列(如「名字」和「分数」配对),以最短的那个为准(多余的丢弃),zip(*matrix) 还能「转置」(行列互换)。为什么不要 range(len(x)):for i in range(len(seq)): x = seq[i] 既啰嗦又慢(每次索引访问),还容易越界/写错——enumerate 更 Pythonic、更快、更清晰。关键点:① 二者都返回「惰性迭代器」(不占内存、只能遍历一次);② zip 取最短、itertools.zip_longest 可补齐最长;③ dict(zip(keys, values)) 快速造字典。核心记忆:要下标用 enumerate(可 start=1)、要并行遍历多个序列用 zip(取最短、可转置),别用 range(len(x))。
详细版
enumerate 和 zip 的用法:
| 工具 | 用法 | 效果 |
|---|---|---|
enumerate(seq) | for i, x in enumerate(seq) | 同时拿下标和值 |
enumerate(seq, 1) | 指定起始编号 | 从 1 开始计数 |
zip(a, b) | for x, y in zip(a, b) | 多序列并行遍历(取最短) |
zip(*matrix) | 解包后配对 | 矩阵转置(行列互换) |
dict(zip(k, v)) | 键值配对成字典 | 快速造字典 |
names = ["Tom", "Amy", "Bob"]
scores = [85, 92, 78]
# enumerate:同时拿索引和元素
for i, name in enumerate(names):
print(i, name) # 0 Tom / 1 Amy / 2 Bob
# start 参数:从 1 开始编号
for rank, name in enumerate(names, start=1):
print(f"第{rank}名: {name}") # 第1名: Tom ...
# zip:并行遍历多个序列
for name, score in zip(names, scores):
print(f"{name}: {score}") # Tom: 85 ...
# zip 取最短
list(zip([1, 2, 3], ['a', 'b'])) # [(1,'a'), (2,'b')](3 被丢)
# zip 转置矩阵
matrix = [[1, 2, 3], [4, 5, 6]]
list(zip(*matrix)) # [(1,4), (2,5), (3,6)](行列互换)
# dict(zip()) 造字典
dict(zip(names, scores)) # {'Tom':85, 'Amy':92, 'Bob':78}
# 对比:别这样写(range(len()) 反模式)
for i in range(len(names)): # ✗ 啰嗦、慢、易错
print(i, names[i])
⚠️ 一句话记住:需要「下标」用
enumerate,需要「同时遍历多个序列」用zip,几乎永远不需要range(len(x))——后者是从 C/Java 带来的思维惯性,在 Python 里既不地道也更慢。range(len(seq))的问题:① 多打字、可读性差(要在循环体里seq[i]再取值);② 慢(每次seq[i]都是一次索引查找,而enumerate/直接迭代是顺序取值);③ 容易出 bug(range(len(seq)-1)、seq[i+1]这类边界很容易错)。更重要的是理解它们的「惰性」本质:enumerate和zip返回的都是迭代器(不是列表),不会一次性构造所有配对、不占额外内存,适合大数据;但代价是只能遍历一次(遍历完就空了,要复用得先list()物化)。zip还有个经典陷阱——取最短序列:如果两个列表长度不等,zip会在最短的用完时停止、丢弃多余的,想「按最长补齐」得用itertools.zip_longest(fillvalue=...)。
完整版教学
一、enumerate:带索引遍历
先掌握 enumerate:
enumerate(iterable, start=0):
遍历时同时产出 (索引, 元素)
→ 每次迭代返回一个 (i, value) 元组
基本用法:
for i, x in enumerate(seq):
# i 是索引(从 0 开始)、x 是元素
→ 元组自动解包成 i 和 x
start 参数:指定索引起始值
enumerate(seq, start=1) # 从 1 开始
enumerate(seq, 100) # 从 100 开始
用途:排名、行号、人类友好的计数(从 1)
产出的是元组:
list(enumerate(['a','b'])) → [(0,'a'), (1,'b')]
不解包也行:for pair in enumerate(seq): pair[0], pair[1]
典型场景:
① 需要下标做定位/记录:
for i, line in enumerate(lines, 1):
if error: print(f"第{i}行出错")
② 边遍历边记位置:
idx = next(i for i, x in enumerate(seq) if x == target)
③ 隔行处理:
for i, x in enumerate(seq):
if i % 2 == 0: ...
所以 enumerate(seq,start=0)遍历同时给(索引,元素),start 定起始编号(排名从 1)
enumerate(iterable, start=0) 遍历时同时产出 (索引, 元素)——每次迭代返回一个 (i, value) 元组。基本用法:for i, x in enumerate(seq)(元组自动解包成 i 和 x)。start 参数:指定索引起始值(enumerate(seq, start=1) 从 1 开始,用于排名、行号、人类友好计数)。产出的是元组:list(enumerate(['a','b'])) → [(0,'a'),(1,'b')]。典型场景:① 需要下标做定位/记录(enumerate(lines, 1) 报第几行出错)、② 边遍历边记位置、③ 隔行处理(if i % 2 == 0)。理解「enumerate(seq,start=0)遍历同时给(索引,元素)、自动解包 i,x;start 定起始编号(排名/行号从 1);产出元组;场景定位/记位置/隔行」,就掌握了 enumerate。
二、zip:并行遍历多个序列
理解 zip 的用法:
zip(*iterables):把多个可迭代对象"按位置配对"
zip(a, b) 每次产出 (a[i], b[i]) 元组
→ 多个序列"并行"遍历
基本用法:
for x, y in zip(list1, list2):
# x 来自 list1、y 来自 list2(同一位置)
多个序列(不止两个):
for a, b, c in zip(l1, l2, l3):
...
产出元组:
list(zip([1,2], ['a','b'])) → [(1,'a'), (2,'b')]
配合 enumerate(既要下标又要配对):
for i, (name, score) in enumerate(zip(names, scores)):
print(i, name, score)
典型场景:
① 配对处理两个相关列表:
for name, age in zip(names, ages): ...
② 造字典:
dict(zip(keys, values))
③ 逐列/逐行处理表格数据
④ 同时迭代多个数据源
对比 range(len):
✗ for i in range(len(names)):
print(names[i], scores[i])
✓ for name, score in zip(names, scores):
print(name, score)
→ zip 更清晰、无需索引
所以 zip(a,b,...)按位置配对多序列并行遍历,产出元组,dict(zip())造字典
zip(*iterables) 把多个可迭代对象「按位置配对」——zip(a, b) 每次产出 (a[i], b[i]) 元组、多个序列并行遍历。基本用法:for x, y in zip(list1, list2)(x 来自 list1、y 来自 list2 同一位置)。多个序列:for a, b, c in zip(l1, l2, l3)。产出元组:list(zip([1,2],['a','b'])) → [(1,'a'),(2,'b')]。配合 enumerate(既要下标又要配对):for i, (name, score) in enumerate(zip(names, scores))。典型场景:① 配对处理两个相关列表、② 造字典 dict(zip(keys, values))、③ 逐列/逐行处理表格、④ 同时迭代多个数据源。对比 range(len):zip(names, scores) 比 range(len(names)) 里 names[i], scores[i] 更清晰。理解「zip(a,b,…)按位置配对多序列并行遍历、产出元组;配合 enumerate 既要下标又要配对;场景配对处理/dict(zip())造字典/表格;比 range(len)清晰」,就掌握了 zip。
三、为什么不要 range(len(x))
理解 range(len(x)) 反模式的问题:
range(len(x)) 反模式(从 C/Java 带来的惯性):
for i in range(len(seq)):
x = seq[i]
...
三个问题:
① 啰嗦、可读性差:
多一行 seq[i],循环体里到处是索引下标
→ enumerate 一步到位拿到 i 和 x
② 慢:
seq[i] 每次都是一次"索引查找"(__getitem__)
直接迭代/enumerate 是"顺序取下一个"(更快)
大数据下差异明显
③ 易出 bug:
range(len(seq)-1)、seq[i+1]、越界... 容易写错
→ 用 enumerate/zip 避免手动算索引
改写对照:
只要值: for x in seq (根本不用索引)
要索引和值: for i, x in enumerate(seq)
并行两个序列: for a, b in zip(la, lb)
要索引配对: for i,(a,b) in enumerate(zip(la,lb))
什么时候确实需要 range(len)(少数):
① 只要索引、不要值,且要复杂的索引运算
(但通常也能用 enumerate 忽略值)
② 原地修改列表且逻辑依赖索引(seq[i] = f(seq[i]))
→ 也可 for i, x in enumerate(seq): seq[i] = f(x)
→ 绝大多数场景都有更 Pythonic 的替代
所以 range(len)啰嗦+慢(索引查找)+易错,用 enumerate/zip/直接迭代替代
range(len(x)) 反模式(从 C/Java 带来的惯性):for i in range(len(seq)): x = seq[i]。三个问题:① 啰嗦、可读性差(多一行 seq[i]、到处是索引下标,enumerate 一步拿到 i 和 x);② 慢(seq[i] 每次是一次索引查找 __getitem__、直接迭代是顺序取下一个更快、大数据差异明显);③ 易出 bug(range(len(seq)-1)、seq[i+1]、越界容易写错)。改写对照:只要值 for x in seq、要索引和值 for i, x in enumerate(seq)、并行两个序列 for a, b in zip(la, lb)、要索引配对 for i,(a,b) in enumerate(zip(la,lb))。什么时候确实需要 range(len)(少数):原地修改列表且依赖索引(也可用 enumerate 配 seq[i] = f(x))。理解「range(len)三问题:啰嗦+慢(索引查找)+易错;改写只要值 for x in seq、要索引 enumerate、并行 zip;少数原地修改可用 enumerate 配 seq[i]=f(x)」,就理解了为什么不要 range(len)。
四、惰性迭代器——只能遍历一次
理解 enumerate/zip 的惰性本质:
enumerate 和 zip 返回"惰性迭代器"(不是列表):
enumerate(seq) → <enumerate object>(迭代器)
zip(a, b) → <zip object>(迭代器)
→ 不一次性构造所有配对,边遍历边产出
好处:
① 省内存:不占额外空间(大数据/无限流友好)
② 惰性:只在遍历时才计算下一个
代价:只能遍历一次(遍历完就空了)
z = zip(a, b)
list(z) → [(...), ...] # 第一次:有内容
list(z) → [] # 第二次:空了!(迭代器耗尽)
for x in z: ... # 第一次遍历
for x in z: ... # 第二次:什么都不发生
要复用 → 先物化成列表:
pairs = list(zip(a, b)) # 存成列表
# 之后可多次遍历 pairs
对比 Python 2:
Py2 的 zip 返回列表(可多次用,但占内存)
Py3 改成迭代器(惰性、省内存,但只能遍历一次)
→ Py2 迁 Py3 时,靠 zip 返回列表的代码要加 list()
判断:
只遍历一次 → 直接用(省内存)
要多次用/要下标访问 → list(zip(...))
所以 enumerate/zip 是惰性迭代器(省内存但只遍历一次),复用要先 list()
enumerate 和 zip 返回「惰性迭代器」(不是列表):enumerate(seq) → enumerate object、zip(a,b) → zip object,不一次性构造所有配对、边遍历边产出。好处:① 省内存(不占额外空间、大数据/无限流友好)、② 惰性(只在遍历时才计算下一个)。代价:只能遍历一次(遍历完就空了)——z = zip(a,b); list(z) 第一次有内容、第二次空了(迭代器耗尽)。要复用先物化成列表:pairs = list(zip(a, b)) 之后可多次遍历。对比 Python 2:Py2 的 zip 返回列表(可多次用但占内存)、Py3 改成迭代器(惰性省内存但只遍历一次),Py2 迁 Py3 时靠 zip 返回列表的代码要加 list()。理解「enumerate/zip 是惰性迭代器(不是列表)、省内存但只能遍历一次(耗尽就空);复用要先 list();Py2 返回列表、Py3 返回迭代器是迁移坑」,就掌握了惰性本质。
五、zip 的取最短陷阱与 zip_longest
理解 zip 长度不等时的行为:
zip 的"取最短"行为:
多个序列长度不等时,zip 以"最短的"为准
最短序列用完就停止,多余的元素被丢弃
zip([1,2,3], ['a','b'])
→ [(1,'a'), (2,'b')] # 3 被丢弃(第二个只有 2 个)
陷阱:
以为 zip 会遍历完最长的 → 实际丢了数据(静默)
data = zip(ids, names, ages) # 若长度不一致,悄悄丢尾部
→ 长度不等且都想要 → 出 bug
要"按最长补齐" → itertools.zip_longest:
from itertools import zip_longest
list(zip_longest([1,2,3], ['a','b'], fillvalue='?'))
→ [(1,'a'), (2,'b'), (3,'?')] # 短的用 fillvalue 补
检查长度一致(Python 3.10+):
zip(a, b, strict=True)
→ 长度不等时抛 ValueError(而不是静默丢弃)
→ 想"必须等长"时用 strict=True 防 bug
zip(*iterable) 解包——转置:
matrix = [[1,2,3],[4,5,6]]
list(zip(*matrix)) → [(1,4),(2,5),(3,6)]
→ * 把每行拆成 zip 的参数,按列配对 = 转置
(不等长时同样取最短,转置要求等长)
所以 zip 取最短(多余静默丢弃),补齐用 zip_longest,等长校验用 strict=True
zip 的「取最短」行为:多个序列长度不等时 zip 以「最短的」为准、最短用完就停止、多余的被丢弃(zip([1,2,3],['a','b']) → [(1,'a'),(2,'b')],3 被丢)。陷阱:以为 zip 遍历完最长的、实际静默丢了数据(长度不等且都想要就出 bug)。要「按最长补齐」用 itertools.zip_longest:zip_longest([1,2,3],['a','b'], fillvalue='?') → [(1,'a'),(2,'b'),(3,'?')]。检查长度一致(Python 3.10+):zip(a, b, strict=True) 长度不等时抛 ValueError(而不是静默丢弃、防 bug)。zip(*iterable) 解包——转置:zip(*matrix) 把每行拆成参数、按列配对 = 转置。理解「zip 取最短(多余静默丢弃是陷阱);补齐用 zip_longest(fillvalue);等长校验用 strict=True(3.10+抛 ValueError);zip(*matrix)转置(按列配对)」,就掌握了 zip 的陷阱与相关工具。
六、总结与实践
总结 enumerate 和 zip:
enumerate 速查:
for i, x in enumerate(seq) # 索引+值
for i, x in enumerate(seq, 1) # 从 1 编号
→ 需要下标时用它,别用 range(len)
zip 速查:
for a, b in zip(la, lb) # 并行遍历
dict(zip(keys, values)) # 造字典
list(zip(*matrix)) # 转置
zip_longest(a, b, fillvalue=x) # 补齐最长
zip(a, b, strict=True) # 校验等长(3.10+)
组合:
for i, (a, b) in enumerate(zip(la, lb)): # 下标+配对
反模式对照(改掉 range(len)):
只要值 → for x in seq
要索引 → for i, x in enumerate(seq)
并行多序列 → for a, b in zip(la, lb)
关键特性:
惰性迭代器(省内存、只遍历一次,复用先 list())
zip 取最短(陷阱,补齐用 zip_longest、校验用 strict)
核心总结:
要下标 → enumerate(可 start=1)
并行遍历多序列 → zip(取最短、可转置、造字典)
别用 range(len(x))(啰嗦、慢、易错)
二者惰性、只遍历一次
所以 enumerate 拿下标、zip 并行遍历,别用 range(len),都是惰性只遍历一次
enumerate 速查:for i, x in enumerate(seq)(索引+值)、enumerate(seq, 1)(从 1 编号)。zip 速查:for a, b in zip(la, lb)(并行遍历)、dict(zip(keys, values))(造字典)、list(zip(*matrix))(转置)、zip_longest(a, b, fillvalue=x)(补齐)、zip(a, b, strict=True)(校验等长)。组合:for i, (a, b) in enumerate(zip(la, lb))。反模式对照:只要值 for x in seq、要索引 enumerate、并行 zip。关键特性:惰性迭代器(省内存、只遍历一次、复用先 list)、zip 取最短(补齐 zip_longest、校验 strict)。理解「enumerate 拿下标(可 start=1)、zip 并行遍历(取最短/转置/造字典);别用 range(len);组合 enumerate(zip());惰性只遍历一次、zip 取最短要注意」,就掌握了总结与实践。
记忆钩子:「enumerate 和 zip 是 Python 遍历两大惯用工具:①enumerate(iterable,start=0)遍历时同时产出(索引,元素),for i,x in enumerate(seq)自动解包,enumerate(seq,start=1)从 1 编号(排名/行号);②zip(a,b,…)把多个序列『按位置配对』并行遍历,for a,b in zip(names,scores),dict(zip(keys,values))造字典,zip(*matrix)转置(按列配对);★别用 range(len(x))——那是 C/Java 惯性,啰嗦(循环体到处 seq[i])+慢(每次索引查找 getitem)+易错(越界/seq[i+1]),改写:只要值 for x in seq、要索引 enumerate、并行 zip;关键特性:enumerate/zip 都是『惰性迭代器』(不是列表)省内存但只能遍历一次(耗尽就空,复用要先 list(),Py3 改成迭代器是 Py2 迁移坑);★zip 陷阱:长度不等时『取最短』多余的静默丢弃,要按最长补齐用 itertools.zip_longest(fillvalue=…),要校验必须等长用 zip(a,b,strict=True)(3.10+不等长抛 ValueError)」。
七、常见误区与追问
- 误区:遍历时要用索引,就得用 range(len(seq))。 不需要——要「索引+值」用
enumerate(seq)(for i, x in enumerate(seq)直接拿到下标和值)、只要值用for x in seq、并行遍历多个序列用zip;range(len(seq))是从 C/Java 带来的思维惯性,在 Python 里更啰嗦(循环体里到处seq[i])、更慢(每次索引查找)、更易出错(边界、越界),几乎总有更 Pythonic 的替代。 - 误区:zip 会遍历完最长的序列。 恰恰相反——zip 以「最短的」序列为准,最短用完就停止、其余序列多余的元素被静默丢弃(
zip([1,2,3],['a','b'])只得两对、3 被丢);如果几个序列长度不等而你以为会全部遍历,就会悄悄丢数据;想「按最长补齐」用itertools.zip_longest(fillvalue=...),想「强制等长否则报错」用zip(a, b, strict=True)(Python 3.10+)。 - 误区:enumerate(seq) 返回的可以反复遍历。 不行——enumerate 和 zip 返回的都是惰性迭代器(不是列表),只能遍历一次、遍历完就耗尽变空了;
z = zip(a, b)之后list(z)第一次有内容、第二次得到空列表;如果需要多次遍历或按索引访问,要先用list()物化成列表(pairs = list(zip(a, b)));这也是 Python 2 到 3 的一个迁移坑(Py2 的 zip 返回列表、Py3 返回迭代器)。 - 误区:enumerate 的索引一定从 0 开始。 默认从 0,但可以用 start 参数改起始值——
enumerate(seq, start=1)从 1 开始编号(适合排名、行号等人类友好的计数)、enumerate(seq, 100)从 100 开始;注意 start 只影响返回的「索引计数」、不影响从序列的哪个位置开始遍历(始终从头遍历整个序列)。 - 追问:enumerate 和 zip 为什么比 range(len()) 更好? 三方面:① 可读性——
for i, x in enumerate(seq)一步拿到索引和值、for a, b in zip(la, lb)直接并行取值,不用在循环体里反复写seq[i]、la[i];② 性能——直接迭代/enumerate 是「顺序取下一个元素」,而seq[i]每次是一次索引查找(__getitem__调用),大数据量下前者更快;③ 更安全——避免手动算索引带来的range(len(seq)-1)、seq[i+1]越界这类边界 bug;此外 enumerate/zip 对任何可迭代对象(不只是支持索引的序列)都能用,而 range(len()) 只能用于有 len 和下标的序列。 - *追问:zip(matrix) 为什么能转置矩阵?
zip(*matrix)里的*是解包操作符,把matrix的每一行(子列表)拆开作为 zip 的多个参数——比如matrix = [[1,2,3],[4,5,6]]经*解包相当于zip([1,2,3], [4,5,6]),zip 按位置配对就得到[(1,4),(2,5),(3,6)],即原来的「第 j 列」变成了新的「第 j 个元组」,实现了行列互换(转置);注意各行长度不等时 zip 取最短、转置结果会丢数据,所以转置要求各行等长;结果是元组的列表,需要列表的列表可以[list(row) for row in zip(*matrix)]。 - 追问:怎么用 zip 快速把两个列表合成字典?多次遍历 zip 结果要注意什么? 用
dict(zip(keys, values)):zip 把 keys 和 values 按位置配成(key, value)元组序列,dict 直接把这些元组变成键值对,比如dict(zip(['a','b'], [1,2]))得{'a':1, 'b':2}(若长度不等取最短、多余丢弃;若 keys 有重复,后面的覆盖前面的);要注意 zip 返回的是一次性迭代器,如果你先z = zip(keys, values)再既想dict(z)又想list(z),第二次会得到空——迭代器已被第一次消费耗尽;需要复用同一个 zip 结果就先pairs = list(zip(keys, values))物化,之后可多次使用。
八、加强记忆
enumerate 和 zip 是 Python 遍历的两大惯用工具:① enumerate(iterable, start=0) 遍历时同时产出 (索引, 元素)——for i, x in enumerate(seq) 自动解包,enumerate(seq, start=1) 从 1 开始编号(排名/行号);② zip(a, b, ...) 把多个序列「按位置配对」并行遍历——for a, b in zip(names, scores)、dict(zip(keys, values)) 造字典、zip(*matrix) 转置(按列配对)。别用 range(len(x))——那是 C/Java 惯性:啰嗦(循环体到处 seq[i])+ 慢(每次索引查找)+ 易错(越界、seq[i+1]);改写:只要值 for x in seq、要索引 enumerate、并行 zip。关键特性:enumerate/zip 都是惰性迭代器(不是列表)——省内存但只能遍历一次(耗尽就空,复用要先 list();Py3 改成迭代器是 Py2 迁移坑)。zip 陷阱:长度不等时**「取最短」、多余的静默丢弃**——按最长补齐用 itertools.zip_longest(fillvalue=...)、强制等长否则报错用 zip(a, b, strict=True)(3.10+)。一句话「要下标用 enumerate(可 start=1)、并行遍历多序列用 zip(取最短、可转置 zip(*m)、造字典 dict(zip()));别用 range(len)(啰嗦慢易错);二者是惰性迭代器只遍历一次(复用先 list());zip 取最短要注意(补齐 zip_longest、校验 strict=True)」。