如何从列表中删除重复项,同时保持顺序?

2022-09-05 01:12:07

如何从列表中删除重复项,同时保持顺序?使用集合删除重复项会破坏原始订单。是否有内置或Pythonic成语?

相关问题:在Python中,从列表中删除重复项的最快算法是什么,以便在保持顺序的同时所有元素都是唯一的?


答案 1

在这里,您有一些选择:http://www.peterbe.com/plog/uniqifiers-benchmark

最快的一个:

def f7(seq):
    seen = set()
    seen_add = seen.add
    return [x for x in seq if not (x in seen or seen_add(x))]

为什么要分配 到 而不是仅仅呼叫 ?Python是一种动态语言,解析每次迭代比解析局部变量的成本更高。 可能在迭代之间发生了变化,并且运行时不够智能,无法排除这种情况。为了安全起见,它必须每次检查对象。seen.addseen_addseen.addseen.addseen.add

如果您计划在同一数据集上大量使用此函数,也许最好使用有序集:http://code.activestate.com/recipes/528878/

O(1) 每个操作的插入、删除和成员检查。

(小的附加说明:总是返回,所以以上只是作为尝试集合更新的一种方式,而不是作为逻辑测试的组成部分。seen.add()None


答案 2

最佳解决方案因 Python 版本和环境约束而异:

Python 3.7+(以及大多数支持3.6的解释器,作为实现细节):

首先在PyPy 2.5.0中引入,并在CPython 3.6中作为实现细节采用,在Python 3.7中成为语言保证之前,plain是插入顺序的,甚至比(也是CPython 3.5实现的C)更有效。因此,到目前为止,最快的解决方案也是最简单的:dictcollections.OrderedDict

>>> items = [1, 2, 0, 1, 3, 2]
>>> list(dict.fromkeys(items))  # Or [*dict.fromkeys(items)] if you prefer
[1, 2, 0, 3]

像这样将所有工作推到C层(在CPython上),但由于s是按插入顺序排列的,因此不会丢失排序。它比(通常需要50-100%的时间长),但比任何其他保持顺序的解决方案都要快得多(大约需要一半的黑客攻击时间,涉及在listcomp中使用sets)。list(set(items))dictdict.fromkeyslist(set(items))

重要提示:来自(见下文)的解决方案在懒惰和支持不可哈希输入项方面具有一些独特的优势;如果您需要这些功能,这是一可行的解决方案。unique_everseenmore_itertools

Python 3.5(以及所有旧版本,如果性能不重要)

正如Raymond所指出的,在CPython 3.5中,在C中实现的丑陋列表理解黑客比(除非你实际上需要最后的列表 - 即使这样,也只有在输入非常短的情况下)因此,在性能和可读性方面,CPython 3.5的最佳解决方案相当于3.6 +使用普通:OrderedDictOrderedDict.fromkeysOrderedDictdict

>>> from collections import OrderedDict
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(OrderedDict.fromkeys(items))
[1, 2, 0, 3]

在CPython 3.4及更早版本上,这将比其他一些解决方案慢,因此,如果分析显示您需要更好的解决方案,请继续阅读。

Python 3.4 及更早版本,如果性能至关重要且第三方模块是可以接受的

正如@abarnert所指出的,more_itertools库 () 包含一个 unique_everseen 函数,该函数旨在解决此问题,而不会在列表推导中出现任何不可读 () 突变。这也是最快的解决方案:pip install more_itertoolsnot seen.add

>>> from more_itertools import unique_everseen
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(unique_everseen(items))
[1, 2, 0, 3]

只需一个简单的库导入,没有黑客攻击。

该模块正在调整迭代工具配方unique_everseen如下所示:

def unique_everseen(iterable, key=None):
    "List unique elements, preserving order. Remember all elements ever seen."
    # unique_everseen('AAAABBBCCDAABBB') --> A B C D
    # unique_everseen('ABBCcAD', str.lower) --> A B C D
    seen = set()
    seen_add = seen.add
    if key is None:
        for element in filterfalse(seen.__contains__, iterable):
            seen_add(element)
            yield element
    else:
        for element in iterable:
            k = key(element)
            if k not in seen:
                seen_add(k)
                yield element

但与配方不同的是,它支持不可哈希的项目(以性能成本;如果中的所有元素都是不可哈希的,则算法变为 , vs。 如果它们都是可哈希的)。itertoolsiterableO(n²)O(n)

重要提示:与此处的所有其他解决方案不同,可以懒惰地使用;峰值内存使用量将相同(最终,底层内存增长到相同的大小),但是如果您不对结果进行迭代,则只需对其进行迭代,您就可以在找到唯一项目时对其进行处理,而不是等到整个输入经过重复数据删除后再处理第一个唯一项目。unique_everseensetlist

Python 3.4 及更早版本,如果性能至关重要第三方模块不可用

您有两种选择:

  1. unique_everseen配方复制并粘贴到您的代码中,并根据上面的示例使用它more_itertools

  2. 使用丑陋的黑客来允许单个listcomp检查和更新,以跟踪所看到的内容:set

    seen = set()
    [x for x in seq if x not in seen and not seen.add(x)]
    

    以依赖丑陋的黑客为代价:

     not seen.add(x)
    

    它依赖于这样一个事实,即始终返回,因此计算结果为 。set.addNonenot NoneTrue

请注意,上述所有解决方案都是(除了调用不可哈希项目的可迭代,即 ,而其他解决方案会立即失败,并带有 a),因此当它们不是最热门的代码路径时,所有解决方案的性能都足够高。使用哪一个取决于你可以依赖哪个版本的语言规范/解释器/第三方模块,性能是否至关重要(不要假设它是关键的;它通常不是),最重要的是,可读性(因为如果维护此代码的人后来陷入杀戮的情绪,你聪明的微优化可能不值得)。O(n)unique_everseenO(n²)TypeError