我把 Python 中 zip() 函数玩出花了!
昨天晚上十一点多,我在家客厅沙发上葛优躺,准备随便刷两道算法题收工,结果一看自己写的代码,全是那种:
for i in range(len(nums)):
a = nums1[i]
b = nums2[i]
...
那一刻突然觉得,哎呀这写得也太“手工”了,像在搬砖。后来我把这段改成了 zip(),整个人都舒坦了,代码一下子就“干净”了很多。就借这个机会跟你唠唠,我是怎么把 Python 里那个看起来很普通的 zip(),慢慢玩出花的。
一、zip() 到底干嘛用的?
先别管什么“迭代器”“打包”这种名词,就当它是个“拉拉手”的工具。
你给它好几列数据,它给你一行行配对好:
names = ['小明', '小红', '小刚']
scores = [95, 88, 76]
for name, score in zip(names, scores):
print(name, score)
输出就是:
小明 95
小红 88
小刚 76
原来你可能写半天下标,现在 zip 一拉手,名字和分数就自动配对上了。算法题里经常要“并行遍历两串东西”,这个时候 zip 基本是第一选择。
二、刷题里最常用的几种玩法
1)并行遍历,告别下标地狱
刷题很常见一个场景:有两个数组,要比较同位置元素,比如:
给两个数组 A、B,统计有多少位置满足 A[i] > B[i]
不用 zip 的版本大概是:
defcount_greater(a, b):
cnt = 0
for i in range(len(a)):
if a[i] > b[i]:
cnt += 1
return cnt
用 zip 改一下:
defcount_greater(a, b):
cnt = 0
for x, y in zip(a, b):
if x > y:
cnt += 1
return cnt
逻辑没变,但“我在比两个数”这件事就特别直观了,不用脑子里再翻译 a[i]、b[i] 各是谁。
2)秒变字典,映射关系一把梭
比如键和值分开存:
keys = ['name', 'age', 'city']
values = ['Tom', 18, 'SZ']
info = dict(zip(keys, values))
print(info) # {'name': 'Tom', 'age': 18, 'city': 'SZ'}
很多算法题会让你把两个列表“映射”起来,比如字符 → 频次、课程名 → 下标,zip 加 dict,直接就搞定了。
再来个稍微贴近题目的例子: 有个字符串 s,还有一个同长度的数组 pos,pos[i] 表示 s[i] 这个字符要去的新位置,让你还原打乱前的字符串。
defrestore(s, pos):
# 把新位置和字符拉到一起
pairs = zip(pos, s)
# 按新位置排序
pairs = sorted(pairs) # 默认按第一个元素排
# 取出排好序的字符
return''.join(ch for _, ch in pairs)
print(restore("code", [2, 0, 3, 1])) # "oced"
这里 zip 起到的作用是:把“新位置”和“字符”绑在一起一起移动。
3)配合 sorted,像高手那样排序
很多题要“按分数排序名字”,或者“按第二维排序一堆东西”,zip 也很好用。
比如有个 names 和 scores,要按分数从高到低排:
names = ['小明', '小红', '小刚']
scores = [95, 88, 76]
# 把分数和名字拉在一起
pairs = zip(scores, names)
# 按分数从大到小排
pairs = sorted(pairs, reverse=True)
for score, name in pairs:
print(name, score)
你甚至可以一步写完:
for score, name in sorted(zip(scores, names), reverse=True):
print(name, score)
刷题平台上那种“排名类”的题,用这一句就显得很利索。
三、矩阵题里,zip 是“转置神器”
矩阵题肯定见过吧,二维数组那种。Python 里最经典的一句就是这行:
transposed = list(zip(*matrix))
比如:
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
transposed = list(zip(*matrix))
print(transposed)
# [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
这其实就是把“按行”变成“按列”。 在很多算法题里,比如“旋转矩阵 90 度”,可以这么写:
defrotate(matrix):
# 先转置
matrix[:] = map(list, zip(*matrix[::-1]))
return matrix
m = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
print(rotate(m))
# [[7, 4, 1],
# [8, 5, 2],
# [9, 6, 3]]
这里有两个小操作:
matrix[::-1]把行倒过来zip(*...)把列和行互换
很多人第一次见这写法觉得像魔法,其实拆开看也没那么玄,就是 zip 在二维数组上的一个非常自然的应用。
四、zip 还能“解压”:从配对恢复多列
有时候题目给的是一堆 pair,比如边表、坐标点:
points = [(1, 2), (3, 4), (5, 6)]
你想单独拿出所有 x 和所有 y,可以反向用 zip:
xs, ys = zip(*points)
print(xs) # (1, 3, 5)
print(ys) # (2, 4, 6)
这招在做“统计所有 x 的最大值”“所有 y 的平均值”这种小操作时特别顺手。
比如求所有点距离原点的最大值:
import math
defmax_dist(points):
xs, ys = zip(*points)
return max(math.hypot(x, y) for x, y in zip(xs, ys))
print(max_dist([(3, 4), (5, 12)])) # 13.0
其实直接在 points 上遍历也行,这里纯粹是演示 zip 解压的味道:它不只是“绑在一起”,还能“拆开来”。
五、长度不一样怎么办?
正常的 zip 遇到长度不一样的,会按最短的截断:
a = [1, 2, 3]
b = [10, 20]
print(list(zip(a, b))) # [(1, 10), (2, 20)]
有些算法题你就是想把缺的那部分当成某个默认值,这时候可以用 itertools.zip_longest:
from itertools import zip_longest
a = [1, 2, 3]
b = [10, 20]
for x, y in zip_longest(a, b, fillvalue=0):
print(x, y)
# 1 10
# 2 20
# 3 0
比如按位相加两个“数字数组”,就可以很轻松地用 zip_longest,把短的那一个自动补 0。
说实话,zip 这个函数本身一点都不复杂,难的是你在写题的时候能不能想到:“这里是不是可以让几个东西拉拉手,一起走?” 我现在刷题有一个小习惯,看到“两个数组一起看”“按列处理二维数组”“排序时带着附加信息走”这几种场景,脑子里会先闪一下 zip,能用就直接上,用久了,代码风格真的会不一样。
行了,先写到这,我去给自己冲杯咖啡,等会儿有空再跟你聊聊 sorted 那些骚…咳,高级用法。