如何从列表中删除重复项,同时保留顺序?使用集合来删除重复项会破坏原始顺序。是否有内置函数或Python惯用语?
如何从列表中删除重复项,同时保留顺序?使用集合来删除重复项会破坏原始顺序。是否有内置函数或Python惯用语?
以下是一些选择: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))]
seen.add
赋值给seen_add
而不是直接调用seen.add
?Python是一种动态语言,每次迭代解析seen.add
的成本比解析局部变量更高。 seen.add
可能在迭代之间发生了改变,运行时无法排除这种可能性。为了安全起见,它必须每次都检查对象。
如果您计划在相同数据集上经常使用此函数,则最好使用有序集:http://code.activestate.com/recipes/528878/
每个操作的O(1)插入、删除和成员检查。seen.add()
始终返回None
,因此上面的or
只是尝试进行设置更新的方式,并不是逻辑测试的必要部分。)>>> items = [1, 2, 0, 1, 3, 2]
>>> list(dict.fromkeys(items)) # Or [*dict.fromkeys(items)] if you prefer
[1, 2, 0, 3]
list(set(items))
这样的操作将所有工作都推到了C层(在CPython上),但由于dict
是按插入顺序排序的,所以dict.fromkeys
不会丢失顺序。它比list(set(items))
慢(通常需要50-100%的时间),但比任何其他保持顺序的解决方案要快得多(大约比涉及在列表推导中使用set
的hack方法快一半)。
重要提示:来自more_itertools
的unique_everseen
解决方案(见下文)在惰性和对非可哈希输入项的支持方面具有独特的优势;如果您需要这些功能,它是唯一可行的解决方案。
>>> from collections import OrderedDict
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(OrderedDict.fromkeys(items))
[1, 2, 0, 3]
在CPython 3.4及更早版本中,这将比其他一些解决方案慢,所以如果性能分析显示你需要更好的解决方案,请继续阅读。
正如@abarnert所指出的,more_itertools
库(pip install more_itertools
)包含一个unique_everseen
函数,该函数被设计用于在列表推导式中不进行任何难以理解的(not 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
将the unique_everseen
recipe复制并粘贴到您的代码中,并按照上面的more_itertools
示例使用它。
使用丑陋的技巧来允许单个列表推导式同时检查和更新一个set
以跟踪已经出现的元素:
seen = set()
[x for x in seq if x not in seen and not seen.add(x)]
这样做的代价是依赖于这个丑陋的技巧:
not seen.add(x)
它依赖于set.add
是一个始终返回None
的原地方法,所以not None
求值为True
。
O(n)
(除了在非可哈希项的可迭代对象上调用unique_everseen
,其时间复杂度为O(n²)
,而其他解决方案会立即出现TypeError
错误)。因此,当它们不是最热门的代码路径时,所有解决方案的性能都足够好。选择使用哪个解决方案取决于您可以依赖的语言规范/解释器/第三方模块的版本,性能是否至关重要(不要假设它是;通常并非如此),以及最重要的是可读性(因为如果稍后维护此代码的人处于杀气腾腾的心情中,您聪明的微小优化可能不值得)。seen = set(seq)
。 - flornquakepip install more_itertools
。 - jamylak>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']
在Python 3.5及以下版本(包括Python 2.7),请使用OrderedDict
。我的测试表明,对于Python 3.5来说,这是各种方法中最快且最短的解决方案(当它获得C实现之后;在3.5之前,它仍然是最清晰的解决方案,尽管不是最快的)。
>>> from collections import OrderedDict
>>> list(OrderedDict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']
set()
将帮助更多的初学者开发可重复的代码。 - Arthur[*dict.fromkeys('abracadabra')]
(解包)代替调用函数 list(...)
怎么样?在我的测试中,这种方法更快,尽管只有非常小的差异可以被检测到。所以我不确定这是否只是巧合。 - colidyreLOAD_GLOBAL
中变得更小);主要优点是避免构造函数路径(需要构造tuple
以获得args
,将NULL
指针作为kwargs
dict
传递,然后分别调用两个几乎为空的__new__
和__init__
,后者必须通过一般化的参数解析代码,以传递0-1个位置参数)。但从3.9开始,list()
通过向量调用协议绕过了大部分这些操作,从而将增量利益从60-70纳秒(3.8.5)降低到20-30纳秒(3.10.0)在我的机器上。 - ShadowRanger不想再多说废话(因为这个问题很老,已经有很多好的回答了),但是我有一个使用pandas解决方案,它在许多情况下非常快速且易于使用。
import pandas as pd
my_list = [0, 1, 2, 3, 4, 1, 2, 3, 5]
>>> pd.Series(my_list).drop_duplicates().tolist()
# Output:
# [0, 1, 2, 3, 4, 5]
more_itertools.unique_everseen
可以做到。 - baxxsequence = ['1', '2', '3', '3', '6', '4', '5', '6']
unique = []
[unique.append(item) for item in sequence if item not in unique]
唯一的 → ['1', '2', '3', '6', '4', '5']
n^2
。 - loopbackbeefor
循环即可。 - jamylakfrom itertools import groupby
[key for key, _ in groupby(sortedList)]
_
是做什么用的? - igorkf我认为如果你想要维持秩序,
list1 = ['b','c','d','b','c','a','a']
list2 = list(set(list1))
list2.sort(key=list1.index)
print list2
list1 = ['b','c','d','b','c','a','a']
list2 = sorted(set(list1),key=list1.index)
print list2
list1 = ['b','c','d','b','c','a','a']
list2 = []
for i in list1:
if not i in list2:
list2.append(i)`
print list2
list1 = ['b','c','d','b','c','a','a']
list2 = []
[list2.append(i) for i in list1 if not i in list2]
print list2
只是想添加另一个(非常高效)来自外部模块的此类功能的实现1:iteration_utilities.unique_everseen
:
>>> from iteration_utilities import unique_everseen
>>> lst = [1,1,1,2,3,2,2,2,1,3,4]
>>> list(unique_everseen(lst))
[1, 2, 3, 4]
我进行了一些时间测试(使用Python 3.6),结果显示它比我测试过的所有其他替代方法都要快,包括OrderedDict.fromkeys
、f7
和more_itertools.unique_everseen
。
%matplotlib notebook
from iteration_utilities import unique_everseen
from collections import OrderedDict
from more_itertools import unique_everseen as mi_unique_everseen
def f7(seq):
seen = set()
seen_add = seen.add
return [x for x in seq if not (x in seen or seen_add(x))]
def iteration_utilities_unique_everseen(seq):
return list(unique_everseen(seq))
def more_itertools_unique_everseen(seq):
return list(mi_unique_everseen(seq))
def odict(seq):
return list(OrderedDict.fromkeys(seq))
from simple_benchmark import benchmark
b = benchmark([f7, iteration_utilities_unique_everseen, more_itertools_unique_everseen, odict],
{2**i: list(range(2**i)) for i in range(1, 20)},
'list size (no duplicates)')
b.plot()
为了确保,我也进行了更多重复测试,以检查是否有差异:
import random
b = benchmark([f7, iteration_utilities_unique_everseen, more_itertools_unique_everseen, odict],
{2**i: [random.randint(0, 2**(i-1)) for _ in range(2**i)] for i in range(1, 20)},
'list size (lots of duplicates)')
b.plot()
还有一个只包含一个值的:
b = benchmark([f7, iteration_utilities_unique_everseen, more_itertools_unique_everseen, odict],
{2**i: [1]*(2**i) for i in range(1, 20)},
'list size (only duplicates)')
b.plot()
在所有这些情况中,iteration_utilities.unique_everseen
函数是最快的(在我的电脑上)。
这个 iteration_utilities.unique_everseen
函数还可以处理输入中的不可哈希值(但其性能为 O(n*n)
而不是当值是可哈希时的 O(n)
性能)。
>>> lst = [{1}, {1}, {2}, {1}, {3}]
>>> list(unique_everseen(lst))
[{1}, {2}, {3}]
1 声明:我是该软件包的作者。
seen_add = seen.add
-- 这对基准测试有必要吗? - Alexdict.fromkeys()
方法? - user3064538more_itertools.unique_everseen
?我不确定它们为什么有相同的名称。 - baxxmore-itertools
,那么使用来自 more-itertools
的函数可能更好、更容易(少依赖)。如果你需要获得最佳性能,那么 iteration_uitilities
在写作时(或至少在写作时)显然更快,如果我正确地阅读图表的话,似乎快了约2倍。如果你不太关心性能:随便用吧。例如,在其他答案中有一些实现非常简短,可能很容易复制和粘贴 :) - MSeifert针对另一个非常老的问题的另一个非常晚的回答:
itertools
recipes函数提供了一个使用seen
集合技术完成此操作的功能,但是:
key
函数。seen.add
来优化循环,而不是N次查找。(f7
也这样做,但某些版本不支持。)ifilterfalse
来优化循环,因此您只需在Python中循环遍历唯一元素,而不是所有元素。(当然,您仍需要在ifilterfalse
内部迭代所有元素,但这是在C中进行的,速度更快。)这个方法比 f7
快吗?这取决于你的数据,所以你需要测试一下。如果你想得到一个列表,f7
使用了一个列表推导式,在这里没有办法做到。 (你可以直接使用 append
而不是 yield
,或者将生成器传递给 list
函数,但两者都无法像列表推导式中的 LIST_APPEND 一样快。)无论如何,通常,挤出几微秒并不像拥有一个易于理解、可重用、已编写的函数那样重要,当你想要装饰时也不需要 DSU。
与所有食谱一样,它也可以在 more-iterools
中找到。
如果你只想要没有 key
的情况,你可以简化它:
def unique(iterable):
seen = set()
seen_add = seen.add
for element in itertools.ifilterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
more-itertools
,这显然是最好的答案。一个简单的from more_itertools import unique_everseen
list(unique_everseen(items))
比我的方法快得多,也比被接受的答案好得多,我认为下载这个库是值得的。我将把我的答案添加到社区wiki中,并加入这个方法。 - jamylak
seen_add
是一项改进,但时间可能会受到系统资源的影响。很想看到完整的计时结果。 - jamylakseen_add = seen.add
和没有使用它只会增加1%的速度,这几乎没有什么意义。 - sleblanc