给定一个对列表
xys
,将其解压为两个列表的 Python 习惯用法是:
xs, ys = zip(*xys)
如果
xys
是一个迭代器,如何将其解压为两个迭代器,而不将所有内容都存储在内存中?
假设你有一些可迭代的对:
a = zip(range(10), range(10))
itertools.tee
: 生成第一和秒的独立迭代器
xs, ys = itertools.tee(a)
xs, ys = (x[0] for x in xs), (y[1] for y in ys)
注意这将保留您迭代其中一个与另一个之间的“差异”。
如果您想独立于另一个迭代器使用一个迭代器,则无法避免将内容拉入内存,因为其中一个迭代器将进行,而另一个则不会(因此必须进行缓冲)。
这样的东西允许您迭代成对的“左侧项目”和“右侧项目”:
import itertools
import operator
it1, it2 = itertools.tee(xys)
xs = map(operator.itemgetter(0), it1))
ys = map(operator.itemgetter(1), it2))
print(next(xs))
print(next(ys))
...但请记住,如果您仅使用一个迭代器,则另一个迭代器将在内存中缓冲项目,直到您开始使用它们。
(顺便说一句,假设 Python 3。在 Python 2 中,您需要使用
itertools.imap()
,而不是 map()
。)
完整答案位于此处。长话短说:我们可以修改
itertools.tee
函数 的 Python 配方,比如
from collections import deque
def unzip(iterable):
"""
Transposes given iterable of finite iterables.
"""
iterator = iter(iterable)
try:
first_elements = next(iterator)
except StopIteration:
return ()
queues = [deque([element])
for element in first_elements]
def coordinate(queue):
while True:
if not queue:
try:
elements = next(iterator)
except StopIteration:
return
for sub_queue, element in zip(queues, elements):
sub_queue.append(element)
yield queue.popleft()
return tuple(map(coordinate, queues))
然后使用它
>>> from itertools import count
>>> zipped = zip(count(), count())
>>> xs, ys = unzip(zipped)
>>> next(xs)
0
以下函数本质上执行与
zip
函数相反的操作。
def unzip(iter, n=2):
iter_copies = itertools.tee(iter, n)
def _gen(i):
for x in iter_copies[i]:
yield x[i]
indv_iters = []
for i in range(n):
indv_iters.append(_gen(i))
return tuple(indv_iters)
这是一个更紧凑的版本:
def unzip(iter, n=2):
iter_copies = itertools.tee(iter, n)
indv_iters = [(lambda i: (x[i] for x in iter_copies[i]))(i) for i in range(n)]
return tuple(indv_iters)
您可以使用以下代码验证它是否有效:
def sample_generator():
for i, j, k in zip(range(10), range(10, 20), range(20, 30)):
yield i, j, k
i_iter, j_iter, k_iter = unzip(sample_generator(), 3)
for i, j, k, reference in zip(i_iter, j_iter, k_iter, range(10)):
assert i == reference
assert j == reference + 10
assert k == reference + 20