Konwersja listy na zbiór zmienia kolejność elementów

119

Ostatnio zauważyłem, że kiedy jestem konwertowania listdo setrzędu elementów ulega zmianie i jest klasyfikowane według charakteru.

Rozważmy ten przykład:

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

Moje pytania to -

  1. Dlaczego to się dzieje?
  2. Jak mogę wykonać operacje na zestawach (zwłaszcza na ustawieniach różnicy) bez utraty pierwotnego zamówienia?
d.putto
źródło
8
Dlaczego nie chcesz stracić pierwotnego zamówienia, zwłaszcza jeśli wykonujesz operacje na zestawach? „Porządek” jest bezsensownym pojęciem dla zbiorów, nie tylko w Pythonie, ale także w matematyce.
Karl Knechtel
131
@KarlKnechtel - Tak "kolejność jest bezsensowną koncepcją zbiorów ... w matematyce", ale mam realne problemy :)
d.putto
Na CPythonie 3.6+ unique = list(dict.fromkeys([1, 2, 1]).keys()). To działa, ponieważ dicts zachowaj zamówienie reklamowe teraz.
Boris

Odpowiedzi:

106
  1. A setto nieuporządkowana struktura danych, więc nie zachowuje kolejności reklamowej.

  2. To zależy od twoich wymagań. Jeśli masz normalną listę i chcesz usunąć jakiś zestaw elementów, zachowując kolejność na liście, możesz to zrobić za pomocą zrozumienia listy:

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

    Jeśli potrzebujesz struktury danych, która obsługuje zarówno szybkie testy członkostwa, jak i zachowanie kolejności wstawiania , możesz użyć kluczy ze słownika Pythona, który począwszy od Pythona 3.7 gwarantuje zachowanie kolejności wstawiania:

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

    btak naprawdę nie trzeba tutaj zamawiać - możesz też użyć set. Pamiętaj, że a.keys() - b.keys()zwraca różnicę zestawu jako a set, więc nie zachowa zamówienia reklamowego.

    W starszych wersjach Pythona możesz collections.OrderedDictzamiast tego użyć :

    >>> 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
źródło
3
Żaden obiekt nie kosztuje 16 bajtów. Jeśli tylko istnieje domyślny OrderedSet (). :(
Sean
2
@Sean nie, oni nie. Nonejest językiem singleton z gwarancją. W CPythonie rzeczywisty koszt jest tylko wskaźnikiem (chociaż ten koszt jest zawsze obecny, ale w przypadku dyktowania można prawie rozważyć, Nonea inne pojedyncze lub udostępniane odniesienia są „bezpłatne”), więc słowo maszynowe, prawdopodobnie 8 bajtów na nowoczesnych komputerach . Ale tak, to nie jest tak wydajne przestrzennie, jak mógłby być zestaw.
juanpa.arrivillaga
2
Na CPythonie 3.6+ możesz to zrobić, dict.fromkeys([1, 2, 1]).keys()ponieważ regularnie dictzachowujesz kolejność.
Boris
@Boris To była tylko część specyfikacji języka, począwszy od Pythona 3.7. Chociaż implementacja CPythona już zachowuje kolejność wstawiania w wersji 3.6, jest to uważane za szczegół implementacji, którego nie mogą przestrzegać inne implementacje Pythona.
Sven Marnach
@ Sven powiedziałem CPython. Publikuję to wszędzie, po prostu mam dość pisania "CPython 3.6 lub jakiejkolwiek innej implementacji zaczynającej się od Pythona 3.7". To nawet nie ma znaczenia, wszyscy używają CPython
Boris
53

W Pythonie 3.6 set()teraz powinno zachować kolejność, ale jest inne rozwiązanie dla Pythona 2 i 3:

>>> x = [1, 2, 20, 6, 210]
>>> sorted(set(x), key=x.index)
[1, 2, 20, 6, 210]
Tiger-222
źródło
8
Dwie uwagi dotyczące zachowania kolejności: tylko od Pythona 3.6, a nawet tam jest uważany za szczegół implementacyjny, więc nie polegaj na nim. Poza tym twój kod jest bardzo nieefektywny, ponieważ za każdym razem, gdy x.indexjest wywoływany, wykonywane jest wyszukiwanie liniowe. Jeśli nie masz nic setprzeciwko złożoności kwadratowej, nie ma powodu, aby używać w pierwszej kolejności.
Thijs van Dien
27
@ThijsvanDien To jest złe, set()nie jest uporządkowane w Pythonie 3.6, nawet jako szczegół implementacji, myślisz o dicts
Chris_Rands
8
@ThijsvanDien Nie, nie są posortowane, chociaż czasami tak się pojawiają, ponieważ intczęsto są
hashami
3
Spróbuj x=[1,2,-1,20,6,210]stworzyć zestaw. Zobaczysz, że nie jest w ogóle uporządkowany, przetestowany w Pythonie 3.6.
GabrielChu,
3
Nie rozumiem, dlaczego ta odpowiedź ma tak wiele pozytywnych głosów, nie utrzymuje kolejności reklamowej ani nie zwraca zestawu.
Igor Rodriguez
20

Odpowiadając na pierwsze pytanie, zbiór to struktura danych zoptymalizowana pod kątem operacji na zbiorach. Podobnie jak zbiór matematyczny, nie wymusza ani nie utrzymuje określonej kolejności elementów. Abstrakcyjna koncepcja zestawu nie wymusza porządku, więc nie jest wymagana realizacja. Kiedy tworzysz zestaw z listy, Python ma swobodę zmiany kolejności elementów na potrzeby wewnętrznej implementacji, której używa dla zbioru, który jest w stanie efektywnie wykonywać operacje na zbiorach.

lvella
źródło
9

usuń duplikaty i zachowaj kolejność za pomocą poniższej funkcji

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

sprawdź ten link

Sana
źródło
Fajny, o wiele lepszy niż moje rozwiązanie :)
Tiger-222
8

W matematyce istnieją zbiory i zbiory uporządkowane (osety).

  • zestaw : nieuporządkowany pojemnik z unikalnymi elementami (zaimplementowany)
  • oset : uporządkowany kontener unikalnych elementów (NotImplemented)

W Pythonie tylko zestawy są implementowane bezpośrednio. Możemy emulować osety zwykłymi klawiszami dyktowania ( 3.7+ ).

Dany

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

Kod

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

Próbny

Repliki są usuwane, kolejność wstawiania zostaje zachowana.

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

Operacje podobne do zestawów na klawiszach dyktowania.

oset - b
# {1, 20, 210}

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

oset & b
# {2, 6}

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

Detale

Uwaga: nieuporządkowana konstrukcja nie wyklucza zamówionych elementów. Raczej nie gwarantuje się zachowania porządku. Przykład:

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

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

Z przyjemnością odkryjesz, że lista i zestaw wielozbiorowy (mset) to dwie bardziej fascynujące, matematyczne struktury danych:

  • lista : uporządkowany kontener elementów, który umożliwia replikacje (wdrożony)
  • mset : nieuporządkowany kontener elementów, który pozwala na replikacje (NotImplemented) *

Podsumowanie

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

* Multiset może być pośrednio emulowany za collections.Counter()pomocą dyktowanego odwzorowania wielokrotności (zliczeń).

pylang
źródło
4

Jak wskazano w innych odpowiedziach, zbiory są strukturami danych (i pojęciami matematycznymi), które nie zachowują kolejności elementów -

Jednak korzystając z kombinacji zestawów i słowników, możliwe jest, że możesz osiągnąć to, co chcesz - spróbuj użyć tych fragmentów:

# 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
źródło
1

Opierając się na odpowiedzi Svena, odkryłem, że używam kolekcji.OrderedDict pomogło mi osiągnąć to, co chcesz, a także pozwoliło mi dodać więcej elementów do dyktu:

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

Jeśli chcesz dodać przedmioty, ale nadal traktować to jak zestaw, możesz po prostu zrobić:

z['nextitem']=None

I możesz wykonać operację taką jak z.keys () na dyktacie i pobrać zestaw:

z.keys()
[1, 2, 20, 6, 210]
jimh
źródło
musisz zrobić, list(z.keys())aby uzyskać listę wyjściową.
jxn
w Pythonie 3, tak. nie w Pythonie 2, chociaż powinienem był określić.
jimh
0

Implementacja powyższej koncepcji najwyższego wyniku, która sprowadza ją z powrotem do listy:

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

Przetestowano (krótko) na Pythonie 3.6 i Pythonie 2.7.

Mike Stucka
źródło
0

Jeśli masz niewielką liczbę elementów na dwóch początkowych listach, na których chcesz wykonać operację ustawiania różnicy, zamiast używać, collections.OrderedDictktóra komplikuje implementację i czyni ją mniej czytelną, możesz użyć:

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

Jego złożoność czasowa nie jest zbyt dobra, ale jest schludna i łatwa do odczytania.

Ultrablendz
źródło
0

Ciekawe, że ludzie zawsze używają „problemu ze świata rzeczywistego”, aby żartować z definicji w naukach teoretycznych.

Jeśli zestaw ma porządek, najpierw musisz rozwiązać następujące problemy. Jeśli Twoja lista zawiera zduplikowane elementy, jaka powinna być kolejność, gdy zamienisz ją w zestaw? Jaka jest kolejność, jeśli połączymy dwa zestawy? Jaka jest kolejność, jeśli przecinamy dwa zbiory o różnej kolejności na tych samych elementach?

Dodatkowo set znacznie szybciej wyszukuje określony klucz, co jest bardzo dobre w działaniu na zestawach (dlatego potrzebny jest zestaw, ale nie lista).

Jeśli naprawdę zależy Ci na indeksie, zachowaj go jako listę. Jeśli nadal chcesz wykonywać operacje na elementach na wielu listach, najprostszym sposobem jest utworzenie słownika dla każdej listy z tymi samymi kluczami w zestawie wraz z wartością listy zawierającą cały indeks klucza z oryginalnej listy.

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
źródło
-8

Oto prosty sposób na zrobienie tego:

x=[1,2,20,6,210]
print sorted(set(x))
Aappu Shankar
źródło
3
Nie oznacza to koniecznie zachowania kolejności.
David Boshton