Python3内置zip函数问题
Python 3.4.2 (default, Oct 8 2014, 13:44:52)
[GCC 4.9.1 20140903 (prerelease)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> gen = (x for x in range(10)) ## Need to wrap range into ()'s to create a generator, next(range(10)) is invalid
>>> list(zip(gen, [1,2,3])) ## zip will "eat up" the number 3
[(0, 1), (1, 2), (2, 3)]
>>> next(gen) ## Here i need next to return 3
4
>>>
问题是我在使用zip函数后丢失了一个值。如果不是因为gen是纯代码,这可能会是个更大的问题。
我不确定是否能创建一个像这样工作的函数。如果zip函数的一个参数是生成器,而其他参数都是“正常”的迭代器(也就是所有值都已知并存储在内存中),那肯定是可以的。如果是这样的话,你只需要最后检查生成器。
基本上,我想知道在Python的标准库中是否有任何函数可以在这种情况下像我需要的那样工作。
当然,在某些情况下,你可以做一些类似的事情
xs = list(gen)
这样你只需要处理一个列表。
我还可以补充一点,从gen中获取zip最后得到的值也是解决这个问题的一种方法。
4 个回答
你可以使用一个包装类来围绕你的生成器,这样就能获取到最新的元素。我大部分代码是从Python的维基百科上拿来的,链接在这里:https://wiki.python.org/moin/Generators。
class gen_wrap(object):
def __init__(self, gen):
self.gen = gen
self.current = None
def __iter__(self):
return self
# Python 3 compatibility
def __next__(self):
return self.next()
def next(self):
self.current = next(self.gen)
return self.current
def last(self):
return self.current
>>> gen = gen_wrap(x for x in range(10))
>>> list(zip(gen, [1,2,3]))
[(0, 1), (1, 2), (2, 3)]
>>> gen.last()
3
问题在于,当 zip
遇到某个可迭代对象的 StopIteration
时,它会忘记之前可迭代对象返回的值。
这里有一个解决方案,使用 zip_longest
和 groupby
,这两个工具来自 itertools
,可以把 zip 的结果分成在最短的可迭代对象结束之前和之后的部分:
>>> from itertools import zip_longest, groupby
>>> sentinel = object()
>>> gen = (x for x in range(10))
>>> g = iter(groupby(zip_longest(gen, [1,2,3], fillvalue=sentinel),
... lambda t: sentinel not in t))
>>> _, before = next(g)
>>> list(before)
[(0, 1), (1, 2), (2, 3)]
>>> _, after = next(g)
>>> next(after)
(3, <object object at 0x7fad64cbf080>)
>>> next(gen)
4
问题在于,zip(gen,[1,2,3])
这个代码会生成 0, 1, 2,还有 3,但是它发现第二个参数的长度只有三。所以如果你把顺序反过来,你可以在next(gen) 这一行代码中生成 3:
>>> gen = (x for x in range(10))
>>> list(zip([1,2,3],gen))
[(1, 0), (2, 1), (3, 2)]
>>> next(gen)
3
不,这里没有内置的函数可以避免这种情况。
发生的事情是,zip()
函数会尝试获取所有输入的下一个值,以便生成下一个元组。它必须按照一定的顺序进行,而这个顺序和你传入的参数是一样的。这一点在文档中是有保证的,具体来说:
可迭代对象的从左到右的评估顺序是有保证的
因为这个函数需要支持任意的可迭代对象,所以 zip()
并不会尝试去确定所有参数的长度。它并不知道你的第二个参数只有3个元素。它只是尝试为每个参数获取下一个值,构建一个元组并返回。如果任何一个参数无法生成下一个值,zip()
的迭代器就结束了。但这意味着它会先询问你的生成器获取下一个元素,然后再询问列表。
除了改变输入的顺序外,你还可以自己构建一个 zip()
函数,这个函数会尝试考虑可用的长度:
def limited_zip(*iterables):
minlength = float('inf')
for it in iterables:
try:
if len(it) < minlength:
minlength = len(it)
except TypeError:
pass
iterators = [iter(it) for it in iterables]
count = 0
while iterators and count < minlength:
yield tuple(map(next, iterators))
count += 1
所以这个版本的 zip()
函数会尝试找出你传入的任何序列的最小长度。这并不能保护你不使用较短的可迭代对象,但对于你的测试案例是有效的:
演示:
>>> gen = iter(range(10))
>>> list(limited_zip(gen, [1, 2, 3]))
[(0, 1), (1, 2), (2, 3)]
>>> next(gen)
3