Python技术迷

我把 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 那些骚…咳,高级用法。