Python3内置zip函数问题

5 投票
4 回答
1712 浏览
提问于 2025-04-29 07:03
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 个回答

1

你可以使用一个包装类来围绕你的生成器,这样就能获取到最新的元素。我大部分代码是从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
1

问题在于,当 zip 遇到某个可迭代对象的 StopIteration 时,它会忘记之前可迭代对象返回的值。

这里有一个解决方案,使用 zip_longestgroupby,这两个工具来自 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
2

问题在于,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
4

不,这里没有内置的函数可以避免这种情况。

发生的事情是,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

撰写回答