关于python:如何在保留顺序的同时从列表中删除重复项?

How do you remove duplicates from a list whilst preserving order?

是否有一个内置的在保留顺序的同时从python的列表中删除重复项?我知道我可以使用集合来删除重复项,但这会破坏原始顺序。我也知道我可以像这样滚动:

1
2
3
4
5
6
def uniq(input):
  output = []
  for x in input:
    if x not in output:
      output.append(x)
  return output

(感谢对该代码示例的释放。)

但如果可能的话,我想利用一个内建的或更为Python式的习语。

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


这里有一些备选方案:http://www.peterbe.com/plog/uniqifiers-benchmark

最快的一个:

1
2
3
4
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,因此上面的或上面的内容只是一种尝试集合更新的方法,而不是逻辑测试的一个组成部分。)


编辑2016

正如Raymond指出的那样,在用C实现OrderedDict的python 3.5+中,列表理解方法将比OrderedDict慢(除非您实际上在末尾需要列表,甚至只有在输入非常短的情况下)。所以3.5+的最佳解决方案是OrderedDict

重要编辑2015

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

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

只需导入一个简单的库,无需进行黑客攻击。这来自ITertools配方unique_everseen的实现,它看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

在python 2.7+中,接受的常见习惯用法(它起作用,但没有针对速度进行优化,我现在将使用unique_everseen,因为它使用collections.OrderedDict

运行时间:O(n)

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

这看起来比:

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

不使用丑陋的黑客:

1
not seen.add(x)

它依赖于这样一个事实:set.add是一个就地方法,总是返回None,所以not NoneTrue进行评估。

但是请注意,尽管hack解决方案具有相同的运行时复杂性o(n),但它的原始速度更快。


在python 2.7中,从iterable中删除重复项的新方法是:

1
2
3
>>> from collections import OrderedDict
>>> list(OrderedDict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']

在Python3.5中,ordereddict有一个C实现。我的计时显示,现在这是Python3.5各种方法中最快和最短的。

在python 3.6中,常规dict既成了有序的又紧凑的。(此功能适用于cpython和pypy,但在其他实现中可能不存在)。这为我们提供了一种新的快速除尘方法,同时保持订单:

1
2
>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']

在Python3.7中,常规dict保证在所有实现中都按顺序排列。因此,最短和最快的解决方案是:

1
2
>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']

对@max的回应:一旦你移动到3.6或3.7并使用常规的dict而不是ordereddict,你就不能以任何其他方式真正击败性能。字典很密集,很容易转换成一个几乎没有开销的列表。目标列表被预先调整为len(d),它保存了列表理解中出现的所有大小。此外,由于内部键列表很密集,复制指针几乎和复制列表一样快。


1
2
3
sequence = ['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']


1
2
from itertools import groupby
[ key for key,_ in groupby(sortedList)]

列表甚至不需要排序,充分的条件是将相等的值分组在一起。

编辑:我假设"保留顺序"意味着列表实际上是有序的。如果不是这样,那么Mizardx的解决方案就是正确的。

社区编辑:然而,这是"将重复的连续元素压缩为单个元素"的最优雅的方法。


不要踢死马(这个问题很古老,已经有了很多好的答案),但这里有一个解决方案,使用熊猫在许多情况下都很快,而且非常容易使用。

1
2
3
4
5
6
7
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]


我想如果你想维持秩序,

您可以尝试以下操作:

1
2
3
4
list1 = ['b','c','d','b','c','a','a']    
list2 = list(set(list1))    
list2.sort(key=list1.index)    
print list2

或者类似地,您可以这样做:

1
2
3
list1 = ['b','c','d','b','c','a','a']  
list2 = sorted(set(list1),key=list1.index)  
print list2

您也可以这样做:

1
2
3
4
5
6
list1 = ['b','c','d','b','c','a','a']    
list2 = []    
for i in list1:    
    if not i in list2:  
        list2.append(i)`    
print list2

也可以这样写:

1
2
3
4
list1 = ['b','c','d','b','c','a','a']    
list2 = []    
[list2.append(i) for i in list1 if not i in list2]    
print list2


对于另一个很晚才回答的老问题:

itertools配方具有这样的功能,使用seen设置技术,但:

  • 处理标准的key功能。
  • 不使用不体面的黑客。
  • 通过预先绑定seen.add而不是查找n次来优化循环。(f7也这样做,但有些版本没有。)
  • 通过使用ifilterfalse来优化循环,因此您只需循环python中的唯一元素,而不是所有元素。(当然,您仍然在ifilterfalse中迭代所有这些函数,但这是在c中进行的,而且速度要快得多。)

它真的比f7快吗?这取决于您的数据,所以您必须测试它并查看。如果你最后想要一个列表,f7使用的是listcomp,这里没有办法做到这一点。(你可以直接用append代替yielding,也可以把生成器输入list函数,但两者都不能像列表附加在listcomp中那样快。)通常,挤出几微秒并不像拥有一个易于理解、可重用、已写的乐趣那么重要。不需要DSU来装饰的操作。

和所有的食谱一样,它也可以在more-iterools中找到。

如果您只想使用no-key案例,可以将其简化为:

1
2
3
4
5
6
def unique(iterable):
    seen = set()
    seen_add = seen.add
    for element in itertools.ifilterfalse(seen.__contains__, iterable):
        seen_add(element)
        yield element


只需从外部模块1添加此类功能的另一个(非常有性能的)实现:iteration_utilities.unique_everseen

1
2
3
4
5
>>> 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.fromkeysf7more_itertools.unique_everseen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
%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()

enter image description here

为了确保我也做了一个有更多重复的测试,只是为了检查它是否有影响:

1
2
3
4
5
6
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()

enter image description here

一个只包含一个值:

1
2
3
4
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()

enter image description here

在所有这些情况下,iteration_utilities.unique_everseen功能是最快的(在我的电脑上)。

iteration_utilities.unique_everseen函数还可以处理输入中不可显示的值(但是,当值可哈希时,使用O(n*n)性能而不是O(n)性能)。

1
2
3
4
>>> lst = [{1}, {1}, {2}, {1}, {3}]

>>> list(unique_everseen(lst))
[{1}, {2}, {3}]

1免责声明:我是那个包裹的作者。


在python 3.7及更高版本中,字典保证记住它们的键插入顺序。这个问题的答案概括了目前的情况。

因此,OrderedDict解决方案已过时,没有任何进口声明,我们只需发布:

1
2
3
>>> lst = [1, 2, 1, 3, 3, 2, 4]
>>> list(dict.fromkeys(lst))
[1, 2, 3, 4]


对于无哈希类型(例如列表列表),基于Mizardx:

1
2
3
def f7_noHash(seq)
    seen = set()
    return [ x for x in seq if str( x ) not in seen and not seen.add( str( x ) )]

5倍更快,减少变型,但更复杂

1
2
3
>>> l = [5, 6, 6, 1, 1, 2, 2, 3, 4]
>>> reduce(lambda r, v: v in r[1] and r or (r[0].append(v) or r[1].add(v)) or r, l, ([], set()))[0]
[5, 6, 1, 2, 3, 4]

说明:

1
2
3
4
5
6
7
8
9
10
11
12
default = (list(), set())
# use list to keep order
# use set to make lookup faster

def reducer(result, item):
    if item not in result[1]:
        result[0].append(item)
        result[1].add(item)
    return result

>>> reduce(reducer, l, default)[0]
[5, 6, 1, 2, 3, 4]

借用haskell的nub函数定义列表时使用的递归思想,这将是一种递归方法:

1
2
def unique(lst):
    return [] if lst==[] else [lst[0]] + unique(filter(lambda x: x!= lst[0], lst[1:]))

例如。:

1
2
In [118]: unique([1,5,1,1,4,3,4])
Out[118]: [1, 5, 4, 3]

我尝试了它来增加数据大小,并看到了次线性时间复杂性(不确定,但建议这对正常数据应该是好的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [122]: %timeit unique(np.random.randint(5, size=(1)))
10000 loops, best of 3: 25.3 us per loop

In [123]: %timeit unique(np.random.randint(5, size=(10)))
10000 loops, best of 3: 42.9 us per loop

In [124]: %timeit unique(np.random.randint(5, size=(100)))
10000 loops, best of 3: 132 us per loop

In [125]: %timeit unique(np.random.randint(5, size=(1000)))
1000 loops, best of 3: 1.05 ms per loop

In [126]: %timeit unique(np.random.randint(5, size=(10000)))
100 loops, best of 3: 11 ms per loop

我还认为有趣的是,这可以很容易地由其他操作推广到唯一性。这样地:

1
2
3
import operator
def unique(lst, cmp_op=operator.ne):
    return [] if lst==[] else [lst[0]] + unique(filter(lambda x: cmp_op(x, lst[0]), lst[1:]), cmp_op)

例如,您可以传入一个函数,该函数使用舍入到相同整数的概念,就像出于唯一性目的是"相等"一样,如下所示:

1
2
def test_round(x,y):
    return round(x) != round(y)

然后,unique(some_list,test_round)将提供列表中的唯一元素,其中unique不再意味着传统的相等性(这意味着使用任何基于集合或基于dict键的方法来解决此问题),而是意味着只取元素可能路由的每个整数k的第一个元素,该元素舍入为k。ND到,例如:

1
2
In [6]: unique([1.2, 5, 1.9, 1.1, 4.2, 3, 4.8], test_round)
Out[6]: [1.2, 5, 1.9, 4.2, 3]


Mizardx的答案提供了多种方法的集合。

这就是我在大声思考时想到的:

1
mylist = [x for i,x in enumerate(mylist) if x not in mylist[i+1:]]


您可以引用一个列表理解,因为它是由符号"[1]"构建的。例如,下面的函数唯一地表示元素列表,而不通过引用其列表理解来更改它们的顺序。

1
2
def unique(my_list):
    return [x for x in my_list if x not in locals()['_[1]']]

演示:

1
2
3
l1 = [1, 2, 3, 4, 1, 2, 3, 4, 5]
l2 = [x for x in l1 if x not in locals()['_[1]']]
print l2

输出:

1
[1, 2, 3, 4, 5]


你可以做一个丑陋的清单理解黑客。

1
[l[i] for i in range(len(l)) if l.index(l[i]) == i]


1
2
3
l = [1,2,2,3,3,...]
n = []
n.extend(ele for ele in l if ele not in set(n))

使用集合的O(1)查找来确定是否在新列表中包含元素的生成器表达式。


一个简单的递归解决方案:

1
2
def uniquefy_list(a):
    return uniquefy_list(a[1:]) if a[0] in a[1:] else [a[0]]+uniquefy_list(a[1:]) if len(a)>1 else [a[0]]

使用_sorted_a numpy阵列的相对有效方法:

1
2
b = np.array([1,3,3, 8, 12, 12,12])    
numpy.hstack([b[0], [x[0] for x in zip(b[1:], b[:-1]) if x[0]!=x[1]]])

输出:

1
array([ 1,  3,  8, 12])


就地方法

这种方法是二次的,因为我们对列表中的每个元素都进行了线性查找(因为dels,我们必须增加重新排列列表的成本)。

也就是说,如果我们从列表的末尾开始,朝着原点移动,就可以在适当的位置操作,删除子列表左侧的每个术语。

代码中的这个想法很简单

1
2
for i in range(len(l)-1,0,-1):
    if l[i] in l[:i]: del l[i]

对实现的简单测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
In [91]: from random import randint, seed                                                                                            
In [92]: seed('20080808') ; l = [randint(1,6) for _ in range(12)] # Beijing Olympics                                                                
In [93]: for i in range(len(l)-1,0,-1):
    ...:     print(l)
    ...:     print(i, l[i], l[:i], end='')
    ...:     if l[i] in l[:i]:
    ...:          print( ': remove', l[i])
    ...:          del l[i]
    ...:     else:
    ...:          print()
    ...: print(l)
[6, 5, 1, 4, 6, 1, 6, 2, 2, 4, 5, 2]
11 2 [6, 5, 1, 4, 6, 1, 6, 2, 2, 4, 5]: remove 2
[6, 5, 1, 4, 6, 1, 6, 2, 2, 4, 5]
10 5 [6, 5, 1, 4, 6, 1, 6, 2, 2, 4]: remove 5
[6, 5, 1, 4, 6, 1, 6, 2, 2, 4]
9 4 [6, 5, 1, 4, 6, 1, 6, 2, 2]: remove 4
[6, 5, 1, 4, 6, 1, 6, 2, 2]
8 2 [6, 5, 1, 4, 6, 1, 6, 2]: remove 2
[6, 5, 1, 4, 6, 1, 6, 2]
7 2 [6, 5, 1, 4, 6, 1, 6]
[6, 5, 1, 4, 6, 1, 6, 2]
6 6 [6, 5, 1, 4, 6, 1]: remove 6
[6, 5, 1, 4, 6, 1, 2]
5 1 [6, 5, 1, 4, 6]: remove 1
[6, 5, 1, 4, 6, 2]
4 6 [6, 5, 1, 4]: remove 6
[6, 5, 1, 4, 2]
3 4 [6, 5, 1]
[6, 5, 1, 4, 2]
2 1 [6, 5]
[6, 5, 1, 4, 2]
1 5 [6]
[6, 5, 1, 4, 2]

In [94]:


如果您经常使用pandas,并且美学优于性能,那么考虑内置功能pandas.Series.drop_duplicates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    import pandas as pd
    import numpy as np

    uniquifier = lambda alist: pd.Series(alist).drop_duplicates().tolist()

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

    alist = np.random.randint(low=0, high=1000, size=10000).tolist()

    print uniquifier(alist) == f7(alist)  # True

时机:

1
2
3
4
    In [104]: %timeit f7(alist)
    1000 loops, best of 3: 1.3 ms per loop
    In [110]: %timeit uniquifier(alist)
    100 loops, best of 3: 4.39 ms per loop

这将保持秩序并在O(n)时间内运行。基本上,我们的想法是在发现复制品的地方制造一个洞,并将其沉入底部。使用读写指针。每当发现重复项时,只有读指针前进,写指针停留在重复项上以覆盖它。

1
2
3
4
5
6
7
8
9
10
11
12
def deduplicate(l):
    count = {}
    (read,write) = (0,0)
    while read < len(l):
        if l[read] in count:
            read += 1
            continue
        count[l[read]] = True
        l[write] = l[read]
        read += 1
        write += 1
    return l[0:write]

如果你需要一个内衬,那么这可能会有帮助:

1
reduce(lambda x, y: x + y if y[0] not in x else x, map(lambda x: [x],lst))

…应该工作,但如果我错了就纠正我


不使用导入模块或集合的解决方案:

1
2
3
4
text ="ask not what your country can do for you ask what you can do for your country"
sentence = text.split("")
noduplicates = [(sentence[i]) for i in range (0,len(sentence)) if sentence[i] not in sentence[:i]]
print(noduplicates)

给出输出:

1
['ask', 'not', 'what', 'your', 'country', 'can', 'do', 'for', 'you']