Python:如何使用set从列表中删除重复项(顺序很重要)

5

我有一个列表:

a = [-11, 13, 13, 10, -11, 10, 9, -3, 6, -9, -6, -6, 13, 8, -11, -5, 6, -8, -12, 5, -9, -1, -5, 2, -2, 13, 14, -9, 7, -4]

使用集合,需要去除重复项,并保持原始顺序。

我使用了以下代码:

def unique(a):
    a = set(a)
    return list(a)

使用它确实可以去除重复项,但问题是它会按数字顺序返回这些项目,就像这样:

>>> unique(a)
[-2, 2, 5, 6, 7, 8, 9, 10, 13, 14, -12, -11, -9, -8, -6, -5, -4, -3, -1]

如何使用集合删除重复项并以原始列表相同的顺序返回它?
编辑:
所以我使用了这段代码,因为它可行:
def unique(a):
    seen = set()
    return [seen.add(x) or x for x in a if x not in seen]

但是有人能解释一下它的作用吗?因为我需要再做一个,但是它返回的列表没有负数,除非我了解这段代码的作用,否则我无法实现。


这个问题被标记为重复,这事实非常具有讽刺意味。 - YM_coding
2个回答

5
这个函数在itertools recipes中已经存在,名为unique_everseen。你可以从那里复制粘贴它,或者阅读它以了解它的工作原理,或者安装第三方软件包more-itertools并从那里使用它。
以下是代码的简化版本:
def unique_everseen(iterable):
    seen = set()
    for element in iterable:
        if element not in seen:
            seen.add(element)
            yield element

食谱中的版本允许使用一个"key"函数,但您不需要它,并且它有两个优化。但首先了解简单版本: "seen"是迄今为止所有值的集合。对于每个值,我们检查它是否在"seen"中。如果是这样,则跳过它。否则,我们将其添加到集合中并"yield"它。因此,我们仅在第一次看到它时"yield"每个元素。
食谱版本中的第一个优化很简单:查找"seen.add"方法不是完全免费的,因此我们只执行一次而不是N次,通过执行"seen_add = seen.add"。在基准测试微不足道的情况下(例如包含小整数的列表),这会产生相当大的差异;而对于更昂贵的哈希值,这可能没有太多的影响。
第二个优化是使用"ifilterfalse"而不是"if"来跳过已经被看到的元素。基本上,这意味着如果您有N个元素和M个唯一元素,则您只在Python中进行M次迭代,在"ifilterfalse"内部的优化C代码中进行N次迭代,而不是在Python中进行N次迭代。由于在C中进行迭代要快得多,所以这是值得的,除非您的大多数元素都是唯一的。
要使其与"key"函数配合使用,您所要做的就是保留一个"key(element)"值的集合,而不是迄今为止看到的"element"值的集合。这使得"ifilterfalse"优化变得更加困难且效果不太明显,因此它未被执行。
如果您只处理序列,而不是任意可迭代对象,并且可以使用Python 2.7+,则有另一种方法可以实现几乎相同的效率,甚至更简单:
def unique(a):
    return OrderedDict.fromkeys(a).keys()

2
滥用列表推导式:
def unique(seq):
    seen = set()
    return [seen.add(x) or x for x in seq if x not in seen]
    # or use parentheses instead of brackets above for a generator

seen.add 总是返回 None,所以这样做不起作用。 - abarnert
编辑后,它确实可以工作,但仍然相当糟糕。在表达式中使用 or 来顺序执行两个操作甚至比使用列表推导进行副作用更加滥用。 - abarnert
实际上,您可以通过将“add”放在条件中来消除对列表推导的滥用:[x for x in seq if x not in seen and not seen.add(x)]。但这仍然是对seen.add的滥用,而且可能更难以以这种方式看到... - abarnert
我觉得 set 可以真正使用一种方法来添加一个项目并返回一个布尔值,指示是否实际需要添加该项目。然后,您可以只需执行例如 [x for x in seq if seen.did_add(x)] - kindall
几乎所有内置类型的方法都不会改变其自身,而是返回一个值...但其中少数几个例外之一,即dict.setdefault,与您提出的set.did_add并不相差太远,因此这样做可能是合理的。 - abarnert

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