如何在保留订单的同时从列表中删除重复项?

Jos*_*ver 722 python list unique duplicates

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

def uniq(input):
  output = []
  for x in input:
    if x not in output:
      output.append(x)
  return output
Run Code Online (Sandbox Code Playgroud)

(感谢您放松代码示例.)

但是如果可能的话,我想利用内置或更多的Pythonic习语.

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

Mar*_*rot 729

在这里你有一些选择: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))]
Run Code Online (Sandbox Code Playgroud)

为什么分配seen.addseen_add的,而不是只调用seen.add?Python是一种动态语言,解析seen.add每次迭代比解析局部变量更昂贵.seen.add可能在迭代之间发生了变化,并且运行时不够聪明,无法排除这种情况.为了安全起见,每次都必须检查对象.

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

O(1)每次操作的插入,删除和成员检查.

  • @JesseDhillon`edove.add`可能在迭代之间发生了变化,而运行时并不够聪明,无法排除这种情况.为了安全起见,每次都必须检查对象. - 如果用`dis.dis(f)`查看字节码,你可以看到它在每次迭代时为`add`成员执行`LOAD_ATTR`.http://ideone.com/tz1Tll (15认同)
  • 对于任何编写 Python 代码的人来说,在牺牲可读性和普遍同意的 Python 约定之前,您真的应该三思而后行,只是为了每个循环多挤出几纳秒。使用和不使用 `seen_add = seen.add` 进行测试只会使速度提高 1%。意义不大。 (6认同)
  • 当我在列表列表上尝试这个时,我得到:TypeError:unhashable type:'list' (5认同)
  • 您的解决方案不是最快的解决方案.在Python 3中(没有测试2)这更快(300k条目列表 - 0.045s(你的)vs 0.035s(这一个):seen = set();如果x没有看到,则返回[x代表行中的x seen.add(x)].我找不到你所做过的seen_add行的任何速度效果. (5认同)
  • @ user136036请链接到您的测试.你运行了多少次?`seen_add`是一种改进,但时间可能会受到系统资源的影响.有兴趣看到完整的时间 (3认同)
  • @prof.phython 它不会影响结果。如果检查失败(“x in saw”),它会更新集合。 (2认同)
  • @Dave *当然*你没有看到有什么大的区别。对于 50,000 个随机整数 0-9,`seen.add` 或 `seen_add` 只会被调用 10 次,这是微不足道的。尝试类似我刚才提到的“seq = list(range(19000))”。 (2认同)
  • 用 C++ 重写它会产生 100% 或更多的改进。我不明白你为什么要花这么多时间在这上面。如果您真正关心亚毫秒级的性能改进,那么从 Python 开始是一个死胡同。 (2认同)

jam*_*lak 328

编辑2016

正如Raymond所指出的那样,在OrderedDict用C语言实现的python 3.5+中,列表理解方法会慢于OrderedDict(除非你实际上需要最后的列表 - 即便如此,只有当输入非常短时).所以3.5+的最佳解决方案是OrderedDict.

重要编辑2015

正如@abarnert指出的那样,more_itertoolslibrary(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]
Run Code Online (Sandbox Code Playgroud)

只需一个简单的库导入,没有黑客攻击.这来自itertools配方的实现,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
Run Code Online (Sandbox Code Playgroud)

在Python中2.7+,可接受的常用习惯用法(虽然可以工作但不是针对速度进行优化,我现在会使用unique_everseen)用于此用途collections.OrderedDict:

运行时间:O(N)

>>> from collections import OrderedDict
>>> items = [1, 2, 0, 1, 3, 2]
>>> list(OrderedDict.fromkeys(items))
[1, 2, 0, 3]
Run Code Online (Sandbox Code Playgroud)

这看起来比以下更好:

seen = set()
[x for x in seq if x not in seen and not seen.add(x)]
Run Code Online (Sandbox Code Playgroud)

并没有利用丑陋的黑客:

not seen.add(x)
Run Code Online (Sandbox Code Playgroud)

它依赖于set.add一个就地方法的事实,它始终返回None所以not None求值True.

但请注意,虽然hack解决方案具有相同的运行时复杂度O(N),但它的原始速度更快.

  • 转换为一些自定义类型的dict只是为了获取密钥?只是另一个拐杖. (5认同)
  • @Nakilon我真的不明白这是怎么回事.它没有暴露任何可变状态,因此它在这个意义上非常干净.在内部,Python集是用dict()(http://stackoverflow.com/questions/3949310/how-is-cpythons-set-implemented)实现的,所以基本上你只是做了解释器无论如何都会做的事情. (3认同)

Ray*_*ger 101

在Python 2.7中,从迭代中删除重复项同时保持原始顺序的新方法是:

>>> from collections import OrderedDict
>>> list(OrderedDict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']
Run Code Online (Sandbox Code Playgroud)

在Python 3.5中,OrderedDict有一个C实现.我的时间表明,现在这是Python 3.5的各种方法中最快和最短的.

在Python 3.6中,常规字典变得有序且紧凑.(此功能适用于CPython和PyPy,但在其他实现中可能不存在).这为我们提供了一种新的最快的扣除方式,同时保留了订单:

>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']
Run Code Online (Sandbox Code Playgroud)

在Python 3.7中,保证常规字典在所有实现中都有序. 因此,最短和最快的解决方案是:

>>> list(dict.fromkeys('abracadabra'))
['a', 'b', 'r', 'c', 'd']
Run Code Online (Sandbox Code Playgroud)

对@max的响应:一旦你移动到3.6或3.7并使用常规字典而不是OrderedDict,你就无法以任何其他方式击败性能.字典很密集,很容易转换成几乎没有开销的列表.目标列表预先调整为len(d),其保存列表推导中出现的所有调整大小.此外,由于内部密钥列表是密集的,因此复制指针几乎快速作为列表副本.

  • 唯一的问题是可迭代的"元素"必须是可散列的 - 对于具有任意元素的iterables(作为列表列表)具有等效性会很好 (5认同)
  • @RaymondHettinger:查找成本很小(3.8 的“LOAD_GLOBAL”变得更小);主要优点是避免构造函数代码路径(需要为“args”构建“元组”并将“NULL”指针作为“kwargs”“dict”传递,然后分别调用大部分空的“__new__”和“__init__”,后者必须经过广义参数解析代码,全部传递 0-1 位置参数)。不过,从 3.9 开始,“list()”通过向量调用协议绕过了大部分,将我的机器上的增量收益从 60-70 ns (3.8.5) 减少到 20-30 ns (3.10.0)。 (2认同)

dan*_*lmo 42

sequence = ['1', '2', '3', '3', '6', '4', '5', '6']
unique = []
[unique.append(item) for item in sequence if item not in unique]
Run Code Online (Sandbox Code Playgroud)

独特→ ['1', '2', '3', '6', '4', '5']

  • 值得注意的是,它运行在`n ^ 2`中 (27认同)
  • 伊克.2次攻击:使用列表进行成员资格测试(慢,O(N))并使用列表理解副作用(在此过程中构建另一个"无"参考列表!) (25认同)
  • 我同意@MartijnPieters 的观​​点,绝对*没有* 理由解释具有副作用的列表。只需使用 `for` 循环代替 (2认同)

Ale*_*der 28

不要踢死马(这个问题很老,而且已经有很多好的答案),但这里有一个使用熊猫的解决方案,在很多情况下都很快,并且使用起来很简单.

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]
Run Code Online (Sandbox Code Playgroud)


Raf*_*ird 24

from itertools import groupby
[ key for key,_ in groupby(sortedList)]
Run Code Online (Sandbox Code Playgroud)

甚至不必对列表进行排序,充分条件是将相等的值组合在一起.

编辑:我认为"保留顺序"意味着列表实际上是有序的.如果不是这种情况,那么MizardX的解决方案是正确的.

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


小智 22

我想如果你想保持秩序,

你可以试试这个:

list1 = ['b','c','d','b','c','a','a']    
list2 = list(set(list1))    
list2.sort(key=list1.index)    
print list2
Run Code Online (Sandbox Code Playgroud)

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

list1 = ['b','c','d','b','c','a','a']  
list2 = sorted(set(list1),key=list1.index)  
print list2 
Run Code Online (Sandbox Code Playgroud)

你也可以这样做:

list1 = ['b','c','d','b','c','a','a']    
list2 = []    
for i in list1:    
    if not i in list2:  
        list2.append(i)`    
print list2
Run Code Online (Sandbox Code Playgroud)

它也可以写成:

list1 = ['b','c','d','b','c','a','a']    
list2 = []    
[list2.append(i) for i in list1 if not i in list2]    
print list2 
Run Code Online (Sandbox Code Playgroud)

  • 大多数答案都集中在表现上.对于不足以担心性能的列表,排序(set(list1),key = list1.index)是我见过的最好的东西.没有额外的导入,没有额外的功能,没有额外的变量,而且它相当简单和可读. (5认同)
  • 您的前两个答案假设可以使用排序功能重建列表的顺序,但这可能不是这样. (3认同)
  • 它也是O(n ^ 2),在已经排序的列表上最坏的情况 (2认同)

tim*_*geb 14

Python 3.7及更高版本中,字典可以保证记住它们的密钥插入顺序.这个问题的答案总结了当前的事态.

因此,OrderedDict解决方案已经过时,如果没有任何导入语句,我们可以简单地发出:

>>> lst = [1, 2, 1, 3, 3, 2, 4]
>>> list(dict.fromkeys(lst))
[1, 2, 3, 4]
Run Code Online (Sandbox Code Playgroud)

  • 请注意,raymonds的回答已经提到了这种方法. (4认同)

aba*_*ert 11

对另一个非常古老的问题的另一个非常晚的答案:

itertools食谱有做到这一点,利用函数seen集技术,但是:

  • 处理标准key功能.
  • 使用没有不合时宜的黑客.
  • 通过预绑定优化循环,seen.add而不是查找N次.(f7也是这样,但有些版本没有.)
  • 通过使用优化循环ifilterfalse,因此您只需循环遍历Python中的唯一元素,而不是所有元素.(ifilterfalse当然,你仍然会在里面迭代所有这些,但那是在C中,并且要快得多.)

它真的比快f7吗?这取决于您的数据,因此您必须对其进行测试并查看.如果你想要一个列表,最后f7使用listcomp,这里没办法做到这一点.(您可以直接append代替yielding,或者您可以将生成器提供给list函数,但是任何一个都不能像listcomp中的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
Run Code Online (Sandbox Code Playgroud)


MSe*_*ert 11

刚添加另一个(非常高性能的)实现的这样的功能从外部模块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]
Run Code Online (Sandbox Code Playgroud)

计时

我做了一些时间安排(Python 3.6),这些显示它比我测试的所有其他选项更快,包括OrderedDict.fromkeys,f7more_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()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

并且只是为了确保我还进行了更多重复的测试,以检查它是否有所作为:

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()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

一个只包含一个值:

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()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

在所有这些情况下,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}]
Run Code Online (Sandbox Code Playgroud)

1免责声明:我是该套餐的作者.


zmk*_*zmk 6

对于没有可清除类型(例如列表列表),基于MizardX:

def f7_noHash(seq)
    seen = set()
    return [ x for x in seq if str( x ) not in seen and not seen.add( str( x ) )]
Run Code Online (Sandbox Code Playgroud)


tim*_*geb 5

pandas 用户应该查看pandas.unique.

>>> import pandas as pd
>>> lst = [1, 2, 1, 3, 3, 2, 4]
>>> pd.unique(lst)
array([1, 2, 3, 4])
Run Code Online (Sandbox Code Playgroud)

该函数返回一个 NumPy 数组。如果需要,您可以使用该方法将其转换为列表tolist

  • `pd.unique(lst).tolist()` 是更好的习惯用法。抄送:@JoeFerndz (2认同)

归档时间:

查看次数:

519581 次

最近记录:

6 年,2 月 前