Blog

Python 中的 enumerate 和 zip

两个内置函数,一个替掉你总在手写的计数器,一个替掉你总是写错的索引。其中一个还会悄悄丢掉数据,不告诉你。

先讲这两个,是因为你天天都在写,也因为本篇最后 zip 做的那件事,正是整个系列后面要回答的问题。

总在手写的计数器

names = ['ada', 'grace', 'alan']

i = 0
for name in names:
    print(i, name)
    i += 1

输出:

0 ada
1 grace
2 alan

三行记账代码,围着一行真正干活的代码。enumerate 把这些全包了:

names = ['ada', 'grace', 'alan']

for i, name in enumerate(names):
    print(i, name)

输出:

0 ada
1 grace
2 alan

enumerate 每一轮给你一对值:先是计数,再是元素。

start 改的是编号,不是位置

names = ['ada', 'grace', 'alan']

for n, name in enumerate(names, start=1):
    print(f"{n}. {name}")

输出:

1. ada
2. grace
3. alan

很多人要的正是它,结果却写成了 i + 1start 不会跳过任何元素——它只改变发出的第一个编号。

别再手动索引

names = ['ada', 'grace', 'alan']

for i in range(len(names)):          # you index on every line that uses the item
    print(i, names[i].upper())

for i, name in enumerate(names):     # the item is already unpacked
    print(i, name.upper())

输出:

0 ADA
1 GRACE
2 ALAN
0 ADA
1 GRACE
2 ALAN

输出一样,但第二种写法不可能把索引弄错。range(len(x)) 值得当成一个小警报:它通常意味着该用 enumerate,有时则说明你根本不需要索引。

zip 同时遍历两个序列

names = ['ada', 'grace', 'alan']
langs = ['analytical engine', 'cobol', 'turing machine']

for name, lang in zip(names, langs):
    print(f"{name:6} {lang}")

输出:

ada    analytical engine
grace  cobol
alan   turing machine

zip 在最短的输入处停下,一声不吭

这个坑能让人白白耗掉一下午。

names  = ['ada', 'grace', 'alan']
scores = [90, 85]

pairs = list(zip(names, scores))
print(pairs)
print(len(names), len(scores), '->', len(pairs))

输出:

[('ada', 90), ('grace', 85)]
3 2 -> 2

Alan 不见了。没有报错,没有警告,你以为有三行,结果只有两行。如果两个列表本来就该一样长,就明说出来,让 Python 去检查:

names  = ['ada', 'grace', 'alan']
scores = [90, 85]

try:
    print(list(zip(names, scores, strict=True)))
except ValueError as err:
    print('ValueError:', err)

输出:

ValueError: zip() argument 2 is shorter than argument 1

strict=True 需要 Python 3.10 或更高版本。只要长度一致是你的假设、而不是碰巧如此,就用上它。

两个值得知道的用法

把键和值 zip 起来,就是一个字典:

cols = ['id', 'name', 'role']
row  = [17, 'ada', 'engineer']

print(dict(zip(cols, row)))

输出:

{'id': 17, 'name': 'ada', 'role': 'engineer'}

还有,zip(*pairs) 能把配对拆开,因为解包这个配对列表之后,每一对都成了单独的参数:

pairs = [('ada', 90), ('grace', 85), ('alan', 71)]

names, scores = zip(*pairs)
print(names)
print(scores)
print(sum(scores) / len(scores))

输出:

('ada', 'grace', 'alan')
(90, 85, 71)
82.0

拿回来的是元组,不是列表。这一点每个人都会意外一次。

两个一起用

names  = ['ada', 'grace', 'alan']
scores = [90, 85, 71]

for n, (name, score) in enumerate(zip(names, scores), start=1):
    print(f"{n}. {name:6} {score}")

输出:

1. ada    90
2. grace  85
3. alan   71

(name, score) 外面的括号不能省。enumerate 给你两样东西,而第二样本身又是一对。

两个都不返回列表

names  = ['ada', 'grace', 'alan']
scores = [90, 85, 71]

z = zip(names, scores)
print(list(z))
print(list(z))

输出:

[('ada', 90), ('grace', 85), ('alan', 71)]
[]

第二次 list(z) 是空的。没有删掉任何东西,也没有任何报错——zip 交回来的东西只能遍历一次,而你已经遍历过了。enumeratemapfilter 和打开的文件都是这样。

这不是 zip 的怪癖。Python 里所有的迭代都是这样工作的,这也是下一篇的内容。

要点

  • enumerate(x, start=1) 代替手写的计数器,也代替 range(len(x))
  • zip 会悄无声息地截断到最短的输入。长度相等只是假设时,传入 strict=True
  • dict(zip(keys, values)) 构建字典;zip(*pairs) 把配对拆开,得到元组。
  • 两者返回的东西都只能遍历一次。一旦需要用两次,马上用 list() 包起来。

这篇文章对你有帮助吗?

点一颗爱心来评分!

平均评分 0 / 5. 投票总数: 0

还没有人投票。来做第一个评分的人吧。