Converter uma lista em um conjunto altera a ordem dos elementos

119

Recentemente, percebi que, quando estou convertendo um listemset a ordem dos elementos é alterado e é classificada pelo personagem.

Considere este exemplo:

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

Minhas perguntas são -

  1. Por que isso está acontecendo?
  2. Como posso fazer operações de configuração (especialmente Set Difference) sem perder o pedido inicial?
d.putto
fonte
8
Por que você não quer perder o pedido inicial, especialmente se estiver fazendo operações de configuração? "ordem" é um conceito sem sentido para conjuntos, não apenas em Python, mas também em matemática.
Karl Knechtel
131
@KarlKnechtel - Sim "ordem é um conceito sem sentido para conjuntos ... em matemática", mas tenho problemas do mundo real :)
d.putto
No CPython 3.6+ unique = list(dict.fromkeys([1, 2, 1]).keys()). Isso funciona porque dicts preserva o pedido de inserção agora.
Boris

Respostas:

105
  1. A seté uma estrutura de dados não ordenada, portanto, não preserva a ordem de inserção.

  2. Isso depende de seus requisitos. Se você tem uma lista normal e deseja remover algum conjunto de elementos preservando a ordem da lista, você pode fazer isso com uma compreensão de lista:

    >>> a = [1, 2, 20, 6, 210]
    >>> b = set([6, 20, 1])
    >>> [x for x in a if x not in b]
    [2, 210]

    Se você precisar de uma estrutura de dados que ofereça suporte a testes rápidos de associação e preservação do pedido de inserção , pode usar as chaves de um dicionário Python, que a partir do Python 3.7 preserva o pedido de inserção:

    >>> 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}

    bnão precisa ser pedido aqui - você também pode usar um set. Observe que a.keys() - b.keys()retorna a diferença definida como umset , portanto, não preservará o pedido de inserção.

    Em versões mais antigas do Python, você pode usar em seu collections.OrderedDictlugar:

    >>> 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)])
Sven Marnach
fonte
3
Nenhum objeto custa 16 bytes. Se apenas houver um OrderedSet () padrão. :(
Sean
2
@Sean não, eles não fazem. Noneé um singleton garantido por linguagem. Em CPython, o custo real é apenas o ponteiro (embora esse custo esteja sempre lá, mas para um dict, você quase pode considerar Nonee outros singletons ou referências compartilhadas "grátis"), então uma palavra de máquina, provavelmente 8 bytes em computadores modernos . Mas sim, não é tão eficiente em termos de espaço quanto um conjunto poderia ser.
juanpa.arrivillaga
2
No CPython 3.6+ você pode simplesmente fazer dict.fromkeys([1, 2, 1]).keys()porque os programas regulares dictpreservam a ordem também.
Boris
@Boris Isso só fez parte da especificação da linguagem a partir do Python 3.7. Embora a implementação do CPython já preserve a ordem de inserção na versão 3.6, isso é considerado um detalhe de implementação que pode não ser seguido por outras implementações do Python.
Sven Marnach
@ Mesmo eu disse CPython. Eu posto isso em todos os lugares, estou cansado de escrever "CPython 3.6 ou qualquer outra implementação começando com Python 3.7". Nem importa, todo mundo está usando CPython
Boris
52

No Python 3.6, set()agora deve manter a ordem, mas há outra solução para Python 2 e 3:

>>> x = [1, 2, 20, 6, 210]
>>> sorted(set(x), key=x.index)
[1, 2, 20, 6, 210]
Tiger-222
fonte
8
Duas notas sobre a preservação de pedidos: apenas a partir do Python 3.6, e mesmo assim, é considerado um detalhe de implementação, então não confie nele. Além disso, seu código é muito ineficiente, pois toda vez que x.indexé chamado, é realizada uma busca linear. Se você está bem com a complexidade quadrática, não há razão para usar um setem primeiro lugar.
Thijs van Dien
27
@ThijsvanDien Isso está errado, set()não está ordenado no Python 3.6, nem mesmo como um detalhe de implementação, você está pensando dicts
Chris_Rands
8
@ThijsvanDien Não, eles não estão classificados, embora às vezes pareçam assim porque eles intcostumam fazer hash stackoverflow.com/questions/45581901/…
Chris_Rands
3
Tente x=[1,2,-1,20,6,210]fazer um conjunto. Você verá que não está ordenado, testado no Python 3.6.
GabrielChu
3
Não consigo entender por que essa resposta tem tantos votos positivos, ela não mantém o pedido de inserção e nem retorna um conjunto.
Igor Rodriguez
20

Respondendo à sua primeira pergunta, um conjunto é uma estrutura de dados otimizada para operações de conjunto. Como um conjunto matemático, ele não impõe ou mantém nenhuma ordem particular dos elementos. O conceito abstrato de um conjunto não impõe ordem, portanto, a implementação não é necessária. Quando você cria um conjunto a partir de uma lista, Python tem a liberdade de alterar a ordem dos elementos de acordo com as necessidades da implementação interna que usa para um conjunto, que é capaz de realizar operações de conjunto de forma eficiente.

lvella
fonte
9

remova duplicatas e preserve a ordem pela função abaixo

def unique(sequence):
    seen = set()
    return [x for x in sequence if not (x in seen or seen.add(x))]

verifique este link

Sana
fonte
Boa, muito melhor do que a minha solução :)
Tiger-222
8

Em matemática, existem conjuntos e conjuntos ordenados (osets).

  • conjunto : um contêiner não ordenado de elementos únicos (implementado)
  • oset : um contêiner ordenado de elementos únicos (NotImplemented)

Em Python, apenas conjuntos são implementados diretamente. Podemos emular osets com teclas dit regulares ( 3.7+ ).

Dado

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

Código

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

Demo

As réplicas são removidas, a ordem de inserção é preservada.

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

Operações semelhantes a conjuntos em teclas dict.

oset - b
# {1, 20, 210}

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

oset & b
# {2, 6}

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

Detalhes

Nota: uma estrutura não ordenada não exclui elementos ordenados. Em vez disso, a ordem mantida não é garantida. Exemplo:

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

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

Pode-se o prazer de descobrir que uma lista e multiset (MSET) são dois, estruturas de dados matemáticos mais fascinantes:

  • lista : um contêiner ordenado de elementos que permite replicações (implementado)
  • mset : um contêiner não ordenado de elementos que permite replicações (NotImplemented) *

Resumo

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

* Um multiset pode ser emulado indiretamente com collections.Counter()um mapeamento de multiplicidades (contagens) semelhante a um dicionário .

pilang
fonte
4

Conforme indicado em outras respostas, conjuntos são estruturas de dados (e conceitos matemáticos) que não preservam a ordem dos elementos -

No entanto, usando uma combinação de conjuntos e dicionários, é possível que você consiga o que deseja - tente usar estes trechos:

# 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
jsbueno
fonte
1

Com base na resposta de Sven, descobri que usar coleções.OrderedDict como me ajudou a realizar o que você deseja e me permite adicionar mais itens ao dict:

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)])

Se você deseja adicionar itens, mas ainda tratá-los como um conjunto, basta fazer:

z['nextitem']=None

E você pode realizar uma operação como z.keys () no dicionário e obter o conjunto:

z.keys()
[1, 2, 20, 6, 210]
Jimh
fonte
você precisa fazer list(z.keys())para obter a saída da lista.
jxn
em Python 3, sim. não em Python 2, embora eu devesse ter especificado.
jimh
0

Uma implementação do conceito de pontuação mais alta acima que o traz de volta à lista:

def SetOfListInOrder(incominglist):
    from collections import OrderedDict
    outtemp = OrderedDict()
    for item in incominglist:
        outtemp[item] = None
    return(list(outtemp))

Testado (brevemente) em Python 3.6 e Python 2.7.

Mike Stucka
fonte
0

Caso você tenha um pequeno número de elementos em suas duas listas iniciais nas quais deseja fazer a operação de definir diferença, em vez de usar o collections.OrderedDictque complica a implementação e a torna menos legível, você pode usar:

# initial lists on which you want to do set difference
>>> nums = [1,2,2,3,3,4,4,5]
>>> evens = [2,4,4,6]
>>> evens_set = set(evens)
>>> result = []
>>> for n in nums:
...   if not n in evens_set and not n in result:
...     result.append(n)
... 
>>> result
[1, 3, 5]

Sua complexidade de tempo não é tão boa, mas é organizada e fácil de ler.

Ultrablendz
fonte
0

É interessante que as pessoas sempre usam 'problema do mundo real' para fazer piada com a definição na ciência teórica.

Se o conjunto estiver em ordem, primeiro você precisa descobrir os seguintes problemas. Se sua lista tiver elementos duplicados, qual deve ser a ordem ao transformá-la em um conjunto? Qual é a ordem se unirmos dois conjuntos? Qual é a ordem se cruzarmos dois conjuntos com ordem diferente nos mesmos elementos?

Além disso, set é muito mais rápido na busca por uma determinada chave, o que é muito bom na operação de conjuntos (e é por isso que você precisa de um conjunto, mas não de uma lista).

Se você realmente se preocupa com o índice, basta mantê-lo como uma lista. Se você ainda deseja fazer a operação de conjunto nos elementos em muitas listas, a maneira mais simples é criar um dicionário para cada lista com as mesmas chaves do conjunto junto com um valor de lista contendo todos os índices da chave na lista original.

def indx_dic(l):
    dic = {}
    for i in range(len(l)):
        if l[i] in dic:
            dic.get(l[i]).append(i)
        else:
            dic[l[i]] = [i]
    return(dic)

a = [1,2,3,4,5,1,3,2]
set_a  = set(a)
dic_a = indx_dic(a)

print(dic_a)
# {1: [0, 5], 2: [1, 7], 3: [2, 6], 4: [3], 5: [4]}
print(set_a)
# {1, 2, 3, 4, 5}
Po-Yao Niu
fonte
-8

Esta é uma maneira fácil de fazer isso:

x=[1,2,20,6,210]
print sorted(set(x))
Aappu Shankar
fonte
3
Isso não preserva a ordem necessariamente.
David Boshton