zip_longest没有fillvalue的用法

17

我正在寻找Python中zipzip_longest函数之间的折衷方案(来自itertools模块),该方案会耗尽所有给定的迭代器,但不会填充任何内容。因此,例如,它应该像这样转置元组:

(11, 12, 13    ),        (11, 21, 31, 41),
(21, 22, 23, 24),  -->   (12, 22, 32, 42),
(31, 32        ),        (13, 23,     43),
(41, 42, 43, 44),        (    24,     44)

(为了更好的图形对齐,添加了空格。)

我通过在zip_longest之后清除fillvalue来构成了一个简单的解决方案。

def zip_discard(*iterables, sentinel = object()):
    return map(
            partial(filter, partial(is_not, sentinel)), 
            zip_longest(*iterables, fillvalue=sentinel))

有没有一种方法可以在一开始不引入哨兵的情况下完成这个操作?是否可以使用yield来改进此方法?哪种方法似乎更有效?


我不确定这个问题是否适合在 [so] 上讨论。你觉得如果你在 Code Review 上发布会更好吗? - Sнаđошƒаӽ
我选择了Stack Overflow,因为这个问题要求最有效的方法,而Python的答案经常包括时间比较。但是由于我自己也不确定,所以我不反对迁移。 - XZS
你是在寻找一个生成器或迭代器的解决方案吗? - Corley Brigman
两者都可以,我只需要一个可迭代的对象以进行进一步处理。 - XZS
2
在使用sentinels时,您希望比较对象identity,因此应该使用op.is_not而不是op.ne - Bakuriu
@Bakuriu 已经更正。谢谢。 - XZS
3个回答

8

zipzip_longest都被设计为生成等长的元组,您可以定义自己的生成器来忽略长度,像这样:

def _one_pass(iters):
    for it in iters:
        try:
            yield next(it)
        except StopIteration:
            pass #of some of them are already exhausted then ignore it.

def zip_varlen(*iterables):
    iters = [iter(it) for it in iterables]
    while True: #broken when an empty tuple is given by _one_pass
        val = tuple(_one_pass(iters))
        if val:
            yield val
        else:
            break

如果要压缩在一起的数据相当大,则每次跳过已用尽的迭代器可能会很耗费资源,因此在_one_pass函数中从iters中移除已完成的迭代器可能更有效率,示例代码如下:

def _one_pass(iters):
    i = 0
    while i<len(iters):
        try:
            yield next(iters[i])
        except StopIteration:
            del iters[i]
        else:
            i+=1

这两个版本都可以消除创建中间结果或使用临时填充值的需要。

7
你的方法很好。我认为使用哨兵是优雅的。也许更符合Python风格的做法是使用嵌套生成器表达式:
def zip_discard_gen(*iterables, sentinel=object()):
    return ((entry for entry in iterable if entry is not sentinel)
            for iterable in zip_longest(*iterables, fillvalue=sentinel))

这需要更少的导入,因为不需要使用partial()ne()

它也稍微快一些:

data = [(11, 12, 13    ),
        (21, 22, 23, 24),
        (31, 32        ),
        (41, 42, 43, 44)]

%timeit [list(x) for x in zip_discard(*data)]  
10000 loops, best of 3: 17.5 µs per loop

%timeit [list(x) for x in zip_discard_gen(*data)]
100000 loops, best of 3: 14.2 µs per loop

编辑

使用列表推导式的版本稍微快一些:

def zip_discard_compr(*iterables, sentinel=object()):
    return [[entry for entry in iterable if entry is not sentinel]
            for iterable in zip_longest(*iterables, fillvalue=sentinel)]

时间:

%timeit zip_discard_compr(*data)
100000 loops, best of 3: 6.73 µs per loop

Python 2版本:

from itertools import izip_longest

SENTINEL = object()

def zip_discard_compr(*iterables):
    sentinel = SENTINEL
    return [[entry for entry in iterable if entry is not sentinel]
            for iterable in izip_longest(*iterables, fillvalue=sentinel)]

时间

这个版本返回与Tadhg McDonald-Jensen的zip_varlen相同的数据结构:

def zip_discard_gen(*iterables, sentinel=object()):
    return (tuple([entry for entry in iterable if entry is not sentinel])
            for iterable in zip_longest(*iterables, fillvalue=sentinel))

它的速度大约快了两倍:

%timeit list(zip_discard_gen(*data))
100000 loops, best of 3: 9.37 µs per loop

%timeit list(zip_varlen(*data))
10000 loops, best of 3: 18 µs per loop

我喜欢这个,但在我的系统上(i5 Haswell笔记本电脑,Python 2.7),列表版本略快一些(6.22微秒对6.62微秒)。另外,zip_discard由于返回列表,不需要真正进行列表转换,因此它的速度再次提高到了5.61微秒。请注意,我确实必须稍微修改代码 - Python 2.7不喜欢*datasentinel关键字参数之前,这会使其使用起来很麻烦...但是,sentinel可以很容易地成为模块全局变量(在定义它的位置)而不会产生任何负面影响... - Corley Brigman
只需将()更改为[],即可将生成器表达式转换为列表推导式。不再需要将结果转换为列表的列表。 - Mike Müller
@CorleyBrigman 添加了一个列表推导式版本。如果你要获取列表的列表,那么它大约快两倍。 - Mike Müller
很棒的解决方案! - MarkS

0
尝试在没有哨兵的情况下解决这个问题(甚至在过渡期内也没有)带来了一些有趣的见解。
m = (
  (11, 12, 13,       ),
  (21, 22, 23, 24, 25),
  (31, 32,           ),
  (41, 42, 43, 44,   ),
)

result = [[r[i] for r in m if r[i:i+1]] for i in range(0, max(len(r) for r in m))]

首先的见解是,我已经使用Python很长时间了,以至于我会不断质疑使用索引的必要性。总有更好的方法。永远都有。
第二个见解是,切片操作返回一个空元组,而通过索引引用可能会导致IndexError。在这个例子中,将r[i:i+1]替换为len(r) < i会起作用。
那么我认为什么是更好的方法呢?使用fillvalue,然后通过过滤器去除必要的值。
t = tuple(zip_longest(*m))
t_clean = [list(filter(None, r)) for r in t]
m_again = tuple(zip_longest(*t))
zip转置习语不容易掌握,但是值得学习。再次转置可以得到原始结果,但是这需要一个缺失的占位符。也可以“跳过”值。考虑到我们都在添加空格以使矩阵易读,我们都希望有占位符存在。如果m是用填充值创建的,它将等于m_again,这是一个很好的特性。优先考虑开发者的性能,这是真正的瓶颈。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接