标题:我把 Python 中 zip() 函数玩出花了!
昨天晚上十一点多,我在公司楼下等外卖,顺手改一个小脚本,本来就是把两个列表拼在一起打印一下,结果一顿操作写了七八行 for 循环,整个人都烦了。抬头一想:我咋不用 zip() 呢?一行就能搞定的事情,被我写成屎山……
所以就想好好把这个小函数从头到尾捋一遍,顺便把我自己踩过的几个坑也说一下。
最常见的场景就是两个列表“结对儿”遍历,比如名字+分数:
names = ["张三", "李四", "王五"]scores = [90, 85, 60]for name, score in zip(names, scores): print(name, "考了", score)输出:
张三 考了 90李四 考了 85王五 考了 60感觉就像给两个列表牵个手,一路一起走。
有时候我还会直接把它变成列表,方便调试看结果:
pairs = list(zip(names, scores))print(pairs) # [('张三', 90), ('李四', 85), ('王五', 60)]这里有两个小点,很容易被忽略掉:
z = zip(names, scores)print(list(z)) # 第一次正常print(list(z)) # 第二次就是 []在 Python3 里,zip() 返回的是迭代器,用一次少一次,用完就没了。调试的时候一不小心就会出现“诶,怎么第二次打印就没东西了”的那种迷惑瞬间。
把数据“拆开”的时候它也能反着来一下
有一次我在处理坐标点 (x, y) 列表,要分别拿到所有 x 和所有 y,本来准备写个 for 循环,后来想到 zip(*iterable) 这个骚操作。
points = [(1, 2), (3, 4), (5, 6)]xs, ys = zip(*points)print(xs) # (1, 3, 5)print(ys) # (2, 4, 6)* 在这儿的意思就是“把列表拆开,按参数一个个传给 zip”,相当于:
zip((1, 2), (3, 4), (5, 6))再来一个稍微“算法一点”的例子:矩阵转置,经常在做题或者处理数据的时候会遇到。
matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9]]transposed = list(zip(*matrix))print(transposed)# [(1, 4, 7), (2, 5, 8), (3, 6, 9)]就这几行,直接把行列互换了,以前我真的是 for 里套 for 在那瞎写,写完还容易下标写错。
把两个列表变成字典,做配置特别顺手
像搞配置的时候,经常有“字段名列表”和“值列表”,用 zip() 拼一下,秒变 dict:
keys = ["name", "age", "city"]values = ["dongge", 18, "Shanghai"]data = dict(zip(keys, values))print(data)# {'name': 'dongge', 'age': 18, 'city': 'Shanghai'}甚至读取 CSV 的时候,也可以这么用:第一行当表头,后面每一行跟表头 zip 一下,是不是就变成一条条记录了。
header = ["id", "name", "score"]row = ["001", "小明", "95"]record = dict(zip(header, row))print(record)# {'id': '001', 'name': '小明', 'score': '95'}这招在写小工具脚本的时候真的很香,尤其是你不想上 pandas 的时候。
和排序、下标这些东西搭一搭配,更好玩
有时候要“按分数排序,然后带着名字一起排”,这个场景特别典型。最蠢的写法是两个列表拆来拆去,其实一行搞定:
names = ["张三", "李四", "王五"]scores = [90, 85, 60]# 按分数从高到低排sorted_pairs = sorted(zip(scores, names), reverse=True)for score, name in sorted_pairs: print(name, score)顺带你还可以把排完序的结果再“拆开”:
sorted_scores, sorted_names = zip(*sorted_pairs)print(sorted_scores) # (90, 85, 60)print(sorted_names) # ('张三', '李四', '王五')有时候我还会把 enumerate 和 zip 混着用,处理两个列表的时候还能顺便拿下标:
a = [10, 20, 30]b = [1, 2, 3]for idx, (x, y) in enumerate(zip(a, b)): print(idx, x, y, x + y)这种写法在刷题的时候非常顺手,既能看下标,又能同时拿到两个列表对应位置的值,代码逻辑也比较清楚。
列表长短不一样怎么办?再上个“加长版”
zip() 有个小小的坑:它会按最短的那个序列截断,多余的直接不要了:
a = [1, 2, 3, 4]b = [10, 20]print(list(zip(a, b))) # [(1, 10), (2, 20)]剩下的 3, 4 就没了。有时候业务上你是允许“不对齐的”,只是想没值的时候补个默认值,这时候可以用 itertools.zip_longest:
from itertools import zip_longesta = [1, 2, 3, 4]b = [10, 20]for x, y in zip_longest(a, b, fillvalue=0): print(x, y)输出:
1 102 203 04 0这个在做对账、对比两个列表差异的时候还挺常用的,至少不会默默“吞”掉后面的数据。
一个是前面说的:zip() 在 Python3 里返回的是迭代器,用一次少一次,调试的时候别一口气 print(list(z)) 打三遍,然后问自己“为啥后面都是空的”。
另一个就是长度不一样时,它按最短的截断这一点,做算法题还好,写业务代码如果不注意,真的可能默默丢数据,而且完全没异常、没错,最难发现那种。
反正我现在写 Python,只要脑子里出现“两堆数据要一一配对”,下意识就会先想一眼 zip() 能不能帮忙。你可以下次写脚本的时候试试,把几个 for 循环删掉,用它重写一遍,说不定会有点小爽感。