将列表转换为集合会改变元素的顺序。

220

最近我注意到,当我将list转换为set时,元素的顺序会被改变,并按字符排序。

考虑以下例子:

x=[1,2,20,6,210]
print(x)
# [1, 2, 20, 6, 210] # the order is same as initial order

set(x)
# set([1, 2, 20, 210, 6]) # in the set(x) output order is sorted

我的问题是 -

  1. 为什么会发生这种情况?
  2. 如何进行集合运算(尤其是集合差)而不会失去初始顺序?

188
@KarlKnechtel - 是的,“对于集合来说,‘顺序’这个概念在数学上是没有意义的”,但我面临现实世界的问题 :) - d.putto
4
在 CPython 3.6+ 中,unique = list(dict.fromkeys([1, 2, 1]).keys()) 可以去除列表中的重复项。这种方法可行是因为现在的字典会保留插入顺序。 - user3064538
16个回答

196
  1. 一个set是一种无序的数据结构,因此它不会保留插入顺序。

  2. 这取决于你的需求。如果你有一个普通的列表,并且想要删除某些元素集合,同时保留列表的顺序,你可以使用列表推导式来实现:

  3. >>> a = [1, 2, 20, 6, 210]
    >>> b = set([6, 20, 1])
    >>> [x for x in a if x not in b]
    [2, 210]
    
    如果你需要一个既支持快速成员测试又能保留插入顺序的数据结构,你可以使用 Python 字典的键。从 Python 3.7 开始,字典会保证按照插入顺序进行排列。
    >>> a = dict.fromkeys([1, 2, 20, 6, 210])
    >>> b = dict.fromkeys([6, 20, 1])
    >>> dict.fromkeys(x for x in a if x not in b)
    {2: None, 210: None}
    

    b 在这里并不需要排序 - 你也可以使用 set。请注意,a.keys() - b.keys() 返回集合差异作为一个 set,因此它将不会保留插入顺序。

    在早期版本的 Python 中,你可以使用 collections.OrderedDict 代替:

    >>> a = collections.OrderedDict.fromkeys([1, 2, 20, 6, 210])
    >>> b = collections.OrderedDict.fromkeys([6, 20, 1])
    >>> collections.OrderedDict.fromkeys(x for x in a if x not in b)
    OrderedDict([(2, None), (210, None)])
    

6
空对象占用16个字节。如果只有默认的OrderedSet()可用就很遗憾。 :( - Sean
6
不,它们不是。 None 是语言保证的单例。在 CPython 中,实际成本仅为指针(尽管这个成本始终存在,但对于字典,您几乎可以将 None 和其他单例或共享引用视为“免费”),因此是一个机器字,现代计算机上可能是8个字节。但是,确实不像集合那样空间有效。 - juanpa.arrivillaga
4
在CPython 3.6+上,您可以只需执行dict.fromkeys([1, 2, 1]).keys(),因为常规的dict也会保留顺序。 - user3064538
@Boris,这只是从Python 3.7开始的语言规范的一部分。虽然在版本3.6中,CPython实现已经保留了插入顺序,但这被认为是实现细节,其他Python实现可能不遵循它。 - Sven Marnach
2
@Sven 我说的是CPython。我到处都发这个帖子,只是厌倦了写“CPython 3.6或任何其他以Python 3.7开头的实现”。其实这也无所谓,因为每个人都在使用CPython。 - user3064538
1
@user3064538(或@Boris)……Sven说得有道理。即使CPython的行为“正确”,那只是一种实现细节。不能保证将来会一直保持这种行为。在我看来,你不应该依赖它,除非你喜欢意外的惊喜。 - undefined

80
在Python 3.6中,set()现在应该保持顺序,但是在Python 2和3中也有另一种解决方案:
>>> x = [1, 2, 20, 6, 210]
>>> sorted(set(x), key=x.index)
[1, 2, 20, 6, 210]

8
关于顺序保留的两点说明:仅适用于 Python 3.6 及以上版本,并且即使在那里,它也被视为实现细节,因此不要依赖它。除此之外,你的代码非常低效,因为每次调用 x.index 都会执行线性搜索。如果你可以接受二次复杂度,则没有使用 set 的理由。 - Thijs van Dien
43
这是错误的,Python 3.6中的set()不是有序的,即使作为一种实现细节也不是,你可能在想dict - Chris_Rands
9
不,它们没有被排序,尽管有时看起来像是排序了,因为整数经常哈希为它们自己。 - Chris_Rands
16
我不明白为什么这个答案有那么多赞,它既没有保持插入顺序,也没有返回一个集合。 - Igor Rodriguez
9
为什么这个有70多个赞,只需要执行一行代码就能得出与你开始输入相同的输出? - Tomerikoo
显示剩余8条评论

48

2
这正是我使用set的原因,而且这解决了使用set从列表中删除重复项时遇到的一个主要问题;即失去原始列表顺序。 - Charles Naccio
1
绝妙的解决方案 - Martin Bucher
我在 return [x for x in sequence if not (tuple(x) in seen or seen.add(tuple(x)))] 进行了修改,因为我需要一个列表里的子列表是唯一的。例如,这样就可以实现 unique([[1, 2, 3], [1, 3, 2], [1, 2, 3]]),原本的方法则不支持。 - dnk8n
这是一个很棒的答案,使用了列表推导式的新方法,我以前从未想过。无论如何,重要的是要记住,它返回的是一个列表而不是集合,因此你会失去快速索引的解决方案。dict.keys()仍然是我首选的方法。 - thethiny

30

回答你的第一个问题,集合是一种为集合操作而优化的数据结构。像数学集合一样,它不强制或维护任何特定的元素顺序。集合的抽象概念不强制排序,因此实现也不需要排序。当你从列表创建一个集合时,Python有权更改元素的顺序以满足集合内部实现的需求,该实现能够高效地执行集合操作。


22

在数学中,有集合全序集(oset)。

  • set:一个无序的唯一元素容器(已实现)
  • oset:一个有序的唯一元素容器(未实现)

在Python中,只直接实现了集合。我们可以使用常规字典键(3.7+)来模拟oset。

要求

a = [1, 2, 20, 6, 210, 2, 1]
b = {2, 6}

代码

oset = dict.fromkeys(a).keys()
# dict_keys([1, 2, 20, 6, 210])

演示

副本被移除,插入顺序保留。

list(oset)
# [1, 2, 20, 6, 210]

在字典的键上执行类似集合的操作。

oset - b
# {1, 20, 210}

oset | b
# {1, 2, 5, 6, 20, 210}

oset & b
# {2, 6}

oset ^ b
# {1, 5, 20, 210}

详情

注:无序结构并不意味着没有有序元素。相反,维护的顺序不能得到保证。例如:

assert {1, 2, 3} == {2, 3, 1}                    # sets (order is ignored)

assert [1, 2, 3] != [2, 3, 1]                    # lists (order is guaranteed)

有人可能会发现,列表多重集合(mset)是两个更加迷人的数学数据结构:

  • 列表:一个允许复制的有序元素容器(已实现)
  • mset:一个允许复制的无序元素容器(未实现)*

摘要

Container | Ordered | Unique | Implemented
----------|---------|--------|------------
set       |    n    |    y   |     y
oset      |    y    |    y   |     n
list      |    y    |    n   |     y
mset      |    n    |    n   |     n*  

*使用类似字典的映射 collections.Counter() 可以间接模拟多重集合,其中存储了元素的重复次数(计数)。


还有部分有序集合(posets)。 - L F
1
而余集,但我认为它们与Python标准库中常见数据结构的主题无关 :) - pylang
非常简洁地解释了。我想获得两个集合的差,并保持其余元素的顺序。使用 dict.fromkeys 并没有完全起作用,但使用 collections 中的 OrderedDict 起作用了。这是使用 Python 3.11.2 实现的。 - Rexovas
没关系...从另一个集合中减去元素后,它仍然不能保持原始顺序。也许我没有理解它应该如何工作。 - Rexovas
@Rexovas 这种技术通过现代字典的特性模拟了oset的属性,即独特的(插入)顺序元素,但最终字典键仍然像集合一样。因此,集合操作会恢复到像集合(无序)一样的行为。 - pylang
@pylang 有道理,我能够使用kindall在这里提到的列表推导式解决方案 https://dev59.com/u2kw5IYBdhLWcg3wSogs - Rexovas

15

使用一行代码即可删除列表中的重复值并保留插入顺序,适用于 Python 3.8.2

mylist = ['b', 'b', 'a', 'd', 'd', 'c']
results = list({value:"" for value in mylist}) print(results) >>> ['b', 'a', 'd', 'c']
results = list(dict.fromkeys(mylist)) print(results) >>> ['b', 'a', 'd', 'c']

3
这是最好的一行代码解决方案。 - SavindraSingh
对于较大的列表,最好使用None而不是空的str。... >>> None.__sizeof__ ()16>>>"".__sizeof__ ()49 - ingyhere
怎么做?它将列表转换为字典,然后一步转换回列表。此外,这在Python 3.7+中可行,因为现在保证了插入顺序。对于大数据集,仅使用字典可以防止内存中出现大量和多个数据结构,这是有益的。 - ingyhere

8

如其他答案所述,集合是一种不保留元素顺序的数据结构(也是数学概念) -

然而,通过使用集合和字典的组合,您可以实现任何想要的功能 - 尝试使用以下代码片段:

# save the element order in a dict:
x_dict = dict(x,y for y, x in enumerate(my_list) )
x_set = set(my_list)
#perform desired set operations
...
#retrieve ordered list from the set:
new_list = [None] * len(new_set)
for element in new_set:
   new_list[x_dict[element]] = element

4

在Sven的回答基础上,我发现使用collections.OrderedDict可以帮助我实现您想要的功能,并允许我向字典中添加更多项目:

import collections

x=[1,2,20,6,210]
z=collections.OrderedDict.fromkeys(x)
z
OrderedDict([(1, None), (2, None), (20, None), (6, None), (210, None)])

如果您想添加项目,但仍希望将其视为集合,则可以执行以下操作:
z['nextitem']=None

您可以对字典执行类似于 z.keys() 的操作,从而获取集合:

list(z.keys())
[1, 2, 20, 6, 210]

1
你需要执行 list(z.keys()) 来获取列表输出。 - jxn
在Python 3中可以,但在Python 2中不行,尽管我应该明确说明。 - jimh

2

还有一种更简单的方法是创建一个空列表,比如“unique_list”,用于添加原始列表中的唯一元素,例如:

unique_list=[]

for i in original_list:
    if i not in unique_list:
        unique_list.append(i)
    else:
        pass

这将为您提供所有唯一的元素,并保持顺序。

1
回答晚了,但你可以使用 Pandas 的 pd.Series 来将列表转换为 Series 对象并保持顺序不变:
import pandas as pd
x = pd.Series([1, 2, 20, 6, 210, 2, 1])
print(pd.unique(x))

输出: 数组[1,2,20,6,210]

适用于字符串列表

x = pd.Series(['c', 'k', 'q', 'n', 'p','c', 'n'])
print(pd.unique(x))

输出结果 ['c' 'k' 'q' 'n' 'p']


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