zip_longest 无填充值

问题描述 投票:0回答:3

我正在Python的

zip
zip_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)

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

我设法通过清除

fillvalue
之后的
zip_longest
来组成一个粗略的解决方案。

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

有没有办法在不引入哨兵的情况下做到这一点?可以使用

yield
来改进吗?哪种方法看起来最有效?

python functional-programming python-itertools
3个回答
8
投票

zip
zip_longest
都被设计为始终生成相等长度的元组,您可以定义自己的生成器,它不关心 len,如下所示:

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

如果压缩在一起的数据相当大,那么每次跳过耗尽的迭代器可能会很昂贵,在

iters
函数中从
_one_pass
中删除已完成的迭代器可能会更有效,如下所示:

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)]

时间

此版本返回与

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

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
即可。

那么我认为更好的方法是什么?使用填充值,然后根据需要过滤掉该值。

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
,这是一个很好的属性。优先考虑开发人员的性能,这是真正的瓶颈。

© www.soinside.com 2019 - 2024. All rights reserved.