文章
合集Python 语言基础第 10 / 21 篇

Python zip() 函数

Summary

zip 的核心在于 * 的逆向操作,它让 zip 同时具备 " 打包 " 和 " 解压 " 两种能力,这种自逆性比较少见。

1. 打包(正常用法)

names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]

pairs = list(zip(names, ages))
# [('Alice', 25), ('Bob', 30), ('Charlie', 35)]

2. 解压(* 的巧妙之处)

# 星号 * 把 pairs "展开"为多个参数
# zip(('Alice', 25), ('Bob', 30), ('Charlie', 35))
names2, ages2 = zip(*pairs)

print(names2)  # ('Alice', 'Bob', 'Charlie')
print(ages2)   # (25, 30, 35)

为什么能这样?

  • zip(a, b, c) 接受多个可迭代对象,横向取元素
  • * 把 [(a1,b1), (a2,b2)] 变成 (a1,b1), (a2,b2) 两个独立参数
  • 于是 zip 重新横向对齐,第一列归第一列,第二列归第二列

3. 矩阵转置(最经典的 zip(*matrix))

这是线性代数中转置操作的一行代码实现:

matrix = [
    [1, 2, 3],   # 第0行
    [4, 5, 6],   # 第1行
    [7, 8, 9]    # 第2行
]

# zip(*matrix) 相当于 zip([1,2,3], [4,5,6], [7,8,9])
transposed = list(zip(*matrix))

# 结果:第0行变第0列,第1行变第1列...
# [(1, 4, 7),   ← 原第0列
#  (2, 5, 8),   ← 原第1列
#  (3, 6, 9)]   ← 原第2列

可视化理解:

原始          *解包展开          zip重新组合
1 2 3    →    1 2 3         →   1 4 7
4 5 6         4 5 6             2 5 8
7 8 9         7 8 9             3 6 9

实用场景:

# 读取CSV后,把行数据转为列数据方便分析
rows = [
    ['Name', 'Age', 'City'],      # 表头
    ['Alice', '25', 'NY'],        # 数据1
    ['Bob', '30', 'LA']           # 数据2
]

# 解压后:每列成为一个元组
columns = list(zip(*rows))
# [('Name', 'Alice', 'Bob'), ('Age', '25', '30'), ('City', 'NY', 'LA')]

# 轻松提取字段
names = columns[0][1:]   # ('Alice', 'Bob')
ages = columns[1][1:]    # ('25', '30')

三、数据分块(Chunking)技巧

这是 zip 最晦涩但实用的用法,利用 * 和 iter 的配合:

data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
n = 3  # 每3个一组

# zip(*[iter(data)]*n) 的解析见下方
chunks = list(zip(*[iter(data)]*n))
# [(1, 2, 3), (4, 5, 6), (7, 8, 9)]

拆解这个黑魔法:

步骤代码结果
1iter(data)创建一个迭代器对象 it
2[iter(data)]*n列表乘法,得到 [it, it, it](同一个迭代器的 3 个引用)
3zip(*[it, it, it])相当于 zip(it, it, it)
4zip 取值第 1 个 it 取走 1,第 2 个 it 取走 2,第 3 个 it 取走 3 → 元组 (1,2,3)
5下一轮第 1 个 it 取走 4... 形成 (4,5,6)

关键:三个 it 是同一个迭代器的引用,zip 横向取时,实际上是在轮流消耗同一个迭代器。

不足 n 个的丢弃问题:

data = [1, 2, 3, 4, 5]
list(zip(*[iter(data)]*3))  # [(1, 2, 3)] —— 4,5 被丢弃

补齐版本(使用 zip_longest):

from itertools import zip_longest

data = [1, 2, 3, 4, 5]
list(zip_longest(*[iter(data)]*3, fillvalue=None))
# [(1, 2, 3), (4, 5, None)]

四、多序列并行处理

zip 让多序列遍历代码变得简洁:

1. 同时修改多个列表(传统 vs zip)

# 传统写法 —— 索引混乱,容易出错
for i in range(len(names)):
    print(f"{names[i]}: {ages[i]} - {cities[i]}")

# zip 写法 —— 结构化解构,一目了然
for name, age, city in zip(names, ages, cities):
    print(f"{name}: {age} - {city}")

2. 字典多键值批量更新

keys = ['a', 'b', 'c']
values = [1, 2, 3]

# 结合 dict 构造
d = dict(zip(keys, values))  # {'a': 1, 'b': 2, 'c': 3}

# 或更新现有字典
for k, v in zip(keys, values):
    d[k] = v * 2

3. 查找差异(对比两个序列)

list1 = [1, 2, 3, 4]
list2 = [1, 2, 4, 4]

# 逐位对比
diff = [(a, b) for a, b in zip(list1, list2) if a != b]
# [(3, 4)] —— 第3个位置不同

五、不等长序列处理

需求方案代码
截短至最短(默认)zipzip(a, b)
填充至最长zip_longestzip_longest(a, b, fillvalue=0)
仅处理等长(严格模式)Python 3.10+ strictzip(a, b, strict=True)

严格模式(Python 3.10+):

# 若长度不等立即报错,防止静默截断导致的数据丢失
for x, y in zip([1, 2], [1, 2, 3], strict=True):
    pass
# ValueError: zip() argument 2 is longer than argument 1

六、记忆口诀

"* 解包是钥匙,zip 既能封也能拆 "

" 一行转置 zip(*M),分块 *[iter]*n 要记牢 "

最复杂的 zip(*[iter(data)]*n) 理解要点:列表里的 n 个迭代器是同一个对象的引用,zip 横向取数时,实际上是在串行消耗同一个数据源。