第 8 条 用 zip
函数同时遍历两个迭代器
在 Python 中,你经常会发现自己有许多与列表相关的对象。列表推导式可以轻松获取源列表并通过应用表达式获取派生列表:
>>> names = ["Cecilia", "Lise", "Marie"]
>>> counts = [len(n) for n in names]
>>> print(counts)
[7, 4, 5]
>>>
派生列表中的元素通过索引与源列表中的元素相关。要同时迭代两个列表,那么可以根据源列表的长度进行迭代:
>>> longest_name = None
>>> max_count = 0
>>> for i in range(len(names)):
... count = counts[i]
... if count > max_count:
... longest_name = names[i]
... max_count = count
...
>>> print(longest_name)
Cecilia
>>>
这种写法的问题在于,必须两次出现循环变量 i
,这样代码变得不太好懂。改用 enumerate
实现可以好一点,但是仍然不够理想:
>>> for i, name in enumerate(names):
... count = counts[i]
... if count > max_count:
... longest_name = name
... max_count = count
...
>>>
为了使这段代码更清晰,Python 提供了 zip
内置函数。zip
使用惰性生成器包装两个或多个迭代器。zip
生成器生成包含每个迭代器的下一个值的元组,这些元组可以直接在 for
语句中解包。生成的代码比索引多个列表的代码清晰得多:
>>> for name, count in zip(names, counts):
... if count > max_count:
... longest_name = name
... max_count = count
...
>>>
zip
每次只从它封装的那些迭代器中各自取出一个元素,这意味着它可以与无限长的输入一起使用,而不会有程序使用过多内存和崩溃的风险。
但是,当输入迭代器的长度不同时,请注意 zip
的行为。
>>> names.append("Rosalind")
>>> for name, count in zip(names, counts):
... print(name)
...
Cecilia
Lise
Marie
>>>
你会发现新添加的名称 Rosalind
没有打印。这就是 zip
的工作原理。它不断产生元组,直到任何一个包装的迭代器耗尽为止。它的输出与最短的输入一样长。当你知道迭代器具有相同的长度时,此方法可以正常工作。
但在许多其他情况下,zip
的截断行为是令人惊讶且糟糕的。如果你不希望传递给 zip
的列表的长度相等,请考虑使用 itertools
内置模块中的 zip_longest
函数:
>>> import itertools
>>>
>>> for name, count in itertools.zip_longest(names, counts, fillvalue=None):
... print(f"{name}: {count}")
...
Cecilia: 7
Lise: 4
Marie: 5
Rosalind: None
>>>
zip_longest
用传递给它的 fillvalue
替换缺失值,默认为 None
。
要点:
zip
内置函数可用于同时迭代多个迭代器。zip
创建一个生成元组的惰性生成器,因此它可以用于无限长的输入。- 如果你为
zip
提供不同长度的迭代器,它会默默地将其输出为最短的迭代器。- 如果你想在不等长的迭代器上使用
zip
而不被截断,请使用itertools
内置模块中的zip_longest
函数。