去除列表中连续重复元素的优雅方式

3
我希望您能提供一种清晰、Pythonic 的方法,以从以下列表中消除:
li = [0, 1, 2, 3, 3, 4, 3, 2, 2, 2, 1, 0, 0]

为了获得所有连续重复元素(长于一个数字的运行),请进行以下操作:

re = [0, 1, 2, 4, 3, 1]

虽然我有可以工作的代码,但它感觉不太符合Python的编程习惯。我非常确定在外面一定有一种更简洁、更优雅的方法来实现我的目标(也许是一些较少知名的itertools函数?)。


你的结果应该是:re = [0, 1, 2, 3, 4, 3, 2, 1, 0],对吗? - jdi
2
@Justin他想要消除长度> 1的群组。 - agf
4个回答

8

这是基于Karl's answer的版本,它不需要列表的副本(tmp、切片和压缩列表)。对于大型列表,izip比(Python 2的)zip快得多。 chain比切片略慢,但不需要tmp对象或列表的副本。使用islice加上制作一个tmp稍微快一点,但需要更多的内存并且不太优雅。

from itertools import izip, chain
[y for x, y, z in izip(chain((None, None), li),
                       chain((None,), li),
                       li) if x != y != z]

一个 timeit 测试表明,对于短组而言,它的速度大约是 Karl 的答案或我最快的 groupby 版本的两倍。

如果您的列表可能包含 None,请确保使用除 None 之外的值(如 object())。

如果您需要它在不是序列的迭代器 / 可迭代对象上工作,或者您的组很长,请使用此版本:

[key for key, group in groupby(li)
        if (next(group) or True) and next(group, None) is None]

timeit 显示,对于 1,000 个项目组,它比其他版本快大约十倍。

之前的慢版本:

[key for key, group in groupby(li) if sum(1 for i in group) == 1]
[key for key, group in groupby(li) if len(tuple(group)) == 1]

@JBernardo 编辑后使用该方法,因为快速的 timeit 表明它比原来的方法快三分之一,并且在 group 较大的情况下不会增加额外的内存使用。谢谢。 - agf
@JBernardo 新版本似乎比sum在短组中更快(以及长组),因为它不需要为每个组创建生成器对象。 - agf
1
我很欣赏你在这里持续的努力。 - Karl Knechtel
@KarlKnechtel 我也意识到我从未测试过你的版本,使用长组——当使用1000个项目组时,它比我的 groupby / next 版本慢了10倍。 - agf

4

agf的答案 如果组的大小很小,那么是不错的选择,但是如果有足够多的重复连续出现,不对这些组进行“加1”将更加有效率。

[key for key, group in groupby(li) if all(i==0 for i,j in enumerate(group)) ]

1
这对于长组是一个很好的优化,但是对于短组来说会慢50%。我加入了一种版本,似乎无论是长组还是短组都更快。 - agf

1
tmp = [object()] + li + [object()]
re = [y for x, y, z in zip(tmp[2:], tmp[1:-1], tmp[:-2]) if y != x and y != z]

1
我还在努力看这是个玩笑还是一个非常糟糕的解决方案... 顺便说一下,它只适用于某些列表。 - JBernardo
2
删除重复值的组相当于保留非重复值,即与相邻值不同的值。tmp在两端都有哨兵,与其他所有元素比较结果都为假。我创建了三个列表:中间一个等同于原始列表,其余两个在每个方向上偏移1。因此,当我使用zip将它们逐个元素进行比较时,相当于将原始列表中的每个元素与其两个邻居进行比较,并保留与任一邻居不同的元素。 - Karl Knechtel
1
@JBernardo 这不仅是一个完全严肃的解决方案,而且这是我立即想到的方法。(实际上,我最初开发了一些更简单的东西,留下了重复元素的唯一副本,然后不得不重新阅读规范...)我想看看你的示例列表,看看它在哪些列表上无法运行。在我的测试中,它可以处理空列表、包含一个object的列表、包含几个唯一object的列表以及包含几个相同object的列表。 - Karl Knechtel
2
@KarlKnechtel 是的,我明白了(尽管我看到 JBernardo 没有:P),但是如果你能解释一下就更好了(把它编辑到你的答案中?)。另外,请注意您可以使用 x!= y!= z - agf
1
@mac 我写了一个混合版本,使用 itertools 来避免制作许多列表的副本。结果比这个版本或我最快的 groupby 版本都要快。请参见我的编辑答案。Karl - 尽管在测试之前它似乎很“幼稚”,但手动进行比较实际上是最快的方法。 - agf
显示剩余4条评论

1
其他解决方案使用了各种itertools辅助工具和推导式,可能看起来更符合Python风格。然而,我进行的快速计时测试显示这个生成器稍微快一些:
_undef = object()

def itersingles(source):
    cur = _undef
    dup = True
    for elem in source:
        if dup:
            if elem != cur:
                cur = elem
                dup = False
        else:
            if elem == cur:
                dup = True
            else:
                yield cur
                cur = elem
    if not dup:
        yield cur

source = [0, 1, 2, 3, 3, 4, 3, 2, 2, 2, 1, 0, 0]
result = list(itersingles(source))

他反复强调他在问题中寻找“简洁”、“优雅”和“Pythonic”。即使没有这样做,除非你知道它是你的性能瓶颈,否则增加更多的代码来节省一点时间是不值得维护的。 - agf
2
我猜我的审美和对“Pythonic”的定义可能与其他人不太一样。就我个人而言,我喜欢一个简单的生成器,它可以快速地遍历可迭代对象,并沿途维护状态;而这在许多其他语言中并不容易实现,但在Python中却很容易表达。另一个解决方案中的“sum(1 for i in group) == 1”对我来说特别浪费(尽管我不知道有更好的方法来实现这一点)。虽然我同意你关于速度与可维护性的观点——如果不是关键点,2倍的加速并不值得。 - Eli Collins
你在sum方面是正确的。受到这一观察和gnibbler版本的启发,我想出了一个版本,似乎对于短列表和长列表都很快,同时利用groupby来使我的自定义代码最小化。 - agf
这与我尝试过的实现相对类似。虽然不完全是我想要的,但你的评论似乎有助于形成我将要接受的答案。谢谢和+1! :) - mac
我最新解决方案的一个版本使用了islice和临时列表,仅比这个解决方案慢25%,而我采用的chain版本仅慢50%。我认为itertools很快,但是当它们不完全符合您的需求时,自定义生成器似乎仍然表现最佳。 - agf

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