如何使用列表推导式从列表中删除重复项?

14

如何使用列表推导式从列表中删除重复项?我有以下代码:

a = [1, 2, 3, 3, 5, 9, 6, 2, 8, 5, 2, 3, 5, 7, 3, 5, 8]
b = []
b = [item for item in a if item not in b]

但是它并不起作用,只是生成了相同的列表。为什么会生成相同的列表?


5
因为执行 if item not in b 时,列表 b 当前是空的。列表推导式会在内存中完成,结果最终被赋值给 b - Felix Kling
那意味着列表推导不像循环那样工作吗? - Alinwndrld
如果您不想使用集合,因为您想保留顺序,请查看itertools recipes中的unique_everseen迭代器。使用方法如下:b = list(unique_everseen(a)) - Lauritz V. Thaulow
2
这有点像一个循环,但它一次性生成结果...这也不是那么令人惊讶。每当你有表达式 x = y 时,y 首先被评估,然后结果被分配给 x。但在评估 y 期间,x 不会被修改。如果你使用 b = list(item for item in a if item not in b),你会有同样的疑问吗? - Felix Kling
8个回答

15

由于在运行时 b 不包含任何元素,因此它生成了一个相同的列表。

你需要的是这个:

>>> a = [1, 2, 3, 3, 5, 9, 6, 2, 8, 5, 2, 3, 5, 7, 3, 5, 8]
>>> b = []
>>> [b.append(item) for item in a if item not in b]
[None, None, None, None, None, None, None, None]
>>> b
[1, 2, 3, 5, 9, 6, 8, 7]

23
谨慎使用列表推导式对旁效果的影响。应改用常规的for循环。 - Lauritz V. Thaulow
这也是一个O(n²)的答案,对于可哈希的输入,可以实现O(n)保留顺序不保留顺序),对于不可哈希但可排序的输入,可以实现O(n log n)(尽管这会将原始顺序替换为排序顺序,除非您在排序和去重中使用其索引装饰和还原输入以及将其纳入排序和去重中,以便第二个排序可以恢复原始顺序)。 - ShadowRanger

12

如果你不介意使用与列表推导式不同的技巧,你可以使用集合:

>>> a = [1, 2, 3, 3, 5, 9, 6, 2, 8, 5, 2, 3, 5, 7, 3, 5, 8]
>>> b = list(set(a))
>>> print b
[1, 2, 3, 5, 6, 7, 8, 9]

我已经查看了set函数,只是想知道上面的代码有什么问题,是否可以进行更正? - Alinwndrld
5
集合不会保留初始顺序...所以请注意这一点。 - Adi Roiban
@AdiRoiban:这个可以通过最小的代码更改来修复。虽然比使用set慢,但如果您使用的是3.6+版本,则速度不会慢(如果您使用的是OrderedDict 3.5及更早版本,则会有更大的影响;运行时间增加了超过3倍,而使用普通dict的3.6+版本仅增加了约66%的运行时间)。 - ShadowRanger

5

使用 a 中的值作为键创建一个 dict,然后使用 keys 方法。

b = dict([(i, 1) for i in a]).keys()

或者使用一个集合:

b = [i for i in set(a)]

4
列表不变的原因是b初始为空。这意味着if item not in b始终为True。只有在列表生成后,才将新的非空列表分配给变量b

1
如果我理解正确的话,这意味着列表推导式会一次性添加项,而不像循环那样逐个检查和添加每个项。 - Alinwndrld
2
@Alinwndrld:我认为这不是一个有效的结论。它只意味着列表推导在赋值之前被评估。该列表可能在内部循环中构建起来。 - CB Bailey

4
使用groupby
>>> from itertools import groupby
>>> a = [1, 2, 3, 3, 5, 9, 6, 2, 8, 5, 2, 3, 5, 7, 3, 5, 8]
>>> [k for k, _ in groupby(sorted(a, key=lambda x: a.index(x)))]
[1, 2, 3, 5, 9, 6, 8, 7]

如果您不关心值在原始列表中的顺序,可以省略key参数,例如:

>>> [k for k, _ in groupby(sorted(a))]
[1, 2, 3, 5, 6, 7, 8, 9]

你可以使用 groupby 来实现一些酷炫的功能。要识别出出现多次的项:

>>> [k for k, v in groupby(sorted(a)) if len(list(v)) > 1]
[2, 3, 5, 8]

或者建立一个频率字典:
>>> {k: len(list(v)) for k, v in groupby(sorted(a))}
{1: 1, 2: 3, 3: 4, 5: 4, 6: 1, 7: 1, 8: 2, 9: 1}

Python的itertools模块中有一些非常有用的函数:其中包括chainteeproduct等。


1
>>> a = [10,20,30,20,10,50,60,40,80,50,40,0,100,30,60]
>>> [a.pop(a.index(i, a.index(i)+1)) for i in a if a.count(i) > 1]
>>> print(a)

1

对于Python 3.6+,相比Niek de Klein的绝大部分优秀解决方案(其主要缺陷是会丢失输入顺序),有一种更好的改进。由于dict现在是插入有序的,你只需要这样做:

b = list(dict.fromkeys(a))

On earlier Python, you'd do:

from collections import OrderedDict

b = list(OrderedDict.fromkeys(a))

虽然 OrderedDict 被移至 C 层,但它仍然没有那么快,因为它保留了许多用于支持重新排序操作的开销,而 dict 不支持这些操作并避免了这些开销。


1
>>> from itertools import groupby
>>> repeated_items = [2,2,2,2,3,3,3,3,4,5,1,1,1]
>>> [
...     next(group)
...     for _, group in groupby(
...         repeated_items,
...         key=repeated_items.index
...     )
... ]
[2, 3, 4, 5, 1]

聪明的解决方案,我喜欢它。缺点是index调用使其成为O(n²),并且假设输入已经分组(它不适用于[2,1,2])。您可以使用修改后的Schwartzian变换(需要from itertools import count, groupby)解决这两个问题,并仍然保留输入顺序:[v for v, _ in sorted([next(grp) for _, grp in groupby(sorted(zip(repeated_items, count())), key=lambda x: x[0])], key=lambda x: x[1])]。可能不值得麻烦,但我喜欢一些由itertools驱动的疯狂。 - ShadowRanger

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