Перетворення списку в набір змінює порядок елементів


119

Нещодавно я помітив, що коли я перетворююсь 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. Як я можу виконувати задані операції (особливо встановити різницю), не втрачаючи початкового порядку?

8
Чому ви не хочете втрачати початкове замовлення, особливо якщо ви робите встановлені операції? "порядок" - безглузде поняття для множин, не тільки в Python, але і в математиці.
Карл Кнечтел

131
@KarlKnechtel - Так "замовлення - це безглузде поняття для множин ... з математики", але у мене є реальні проблеми світу :)
d.putto

На CPython 3.6+ unique = list(dict.fromkeys([1, 2, 1]).keys()). Це працює, оскільки dictзберегти порядок вставки зараз.
Борис

Відповіді:


106
  1. A set- це не упорядкована структура даних, тому вона не зберігає порядок вставки.

  2. Це залежить від ваших вимог. Якщо у вас є звичайний список і ви хочете видалити деякий набір елементів, зберігаючи порядок списку, ви можете зробити це з розумінням списку:

    >>> 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()повертає задану різницю як a 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)])

3
Жоден об'єкт не коштує 16 байт. Якщо є лише OrdersSet () за замовчуванням. :(
Шон

2
@Sean ні, вони ні. None- це мова, гарантована одинокою. У CPython фактична вартість - це лише вказівник (хоча ця вартість завжди є, але для диктату ви можете майже вважати Noneй інші сингтони або спільні посилання "безкоштовно"), тож машинне слово, ймовірно, 8 байт на сучасних комп'ютерах . Але так, це не настільки ефективно, як набір.
juanpa.arrivillaga

2
У CPython 3.6+ ви можете просто так робити, dict.fromkeys([1, 2, 1]).keys()оскільки регулярно dictзберігати порядок також.
Борис

@Boris Це лише частина мовної специфікації, починаючи з Python 3.7. Хоча реалізація CPython вже зберігає порядок вставки у версії 3.6, це вважається деталлю реалізації, за яким можуть не дотримуватися інші реалізації Python.
Свен Марнах

@Sven Я сказав CPython. Я публікую це скрізь, я просто втомився писати "CPython 3.6 або будь-яку іншу реалізацію, починаючи з Python 3.7". Навіть не важливо, всі користуються CPython
Борис

52

У 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

27
@ThijsvanDien Це неправильно, set()не впорядковано в Python 3.6, навіть не як деталь реалізації, про яку ви думаєте dicts
Chris_Rands

8
@ThijsvanDien Ні , вони не сортують, хоча іноді з'являються так , тому що ints часто хеш себе stackoverflow.com/questions/45581901 / ...
Chris_Rands

3
Спробуйте x=[1,2,-1,20,6,210]і зробіть це набором. Ви побачите, що це зовсім не замовлено, протестовано в Python 3.6.
ГабріельЧу

3
Я не можу зрозуміти, чому ця відповідь має стільки відгуків, вона не підтримує порядок вставки, а також не повертає набір.
Ігор Родрігес

20

Відповідаючи на ваше перше запитання, набір - це структура даних, оптимізована для заданих операцій. Як і математичний набір, він не примушує і не підтримує особливого порядку елементів. Абстрактне поняття набору не примусово виконує порядок, тому його виконання не потрібно. Коли ви створюєте набір зі списку, Python має право змінювати порядок елементів для потреб внутрішньої реалізації, яку він використовує для набору, який здатний ефективно виконувати задані операції.



8

У математиці є множини і впорядковані множини (осети).

  • набір : не упорядкований контейнер унікальних елементів (реалізовано)
  • oset : замовлений контейнер унікальних елементів (NotImplemented)

У Python безпосередньо реалізуються лише набори. Ми можемо емулювати осети звичайними клавішами dict ( 3.7+ ).

Дано

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]

Набір операцій на клавішах dict.

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) - це ще дві захоплюючі, математичні структури даних:

  • list : упорядкований контейнер елементів, що дозволяє репліки (реалізовано)
  • mset : не упорядкований контейнер елементів, що дозволяє копіювати (NotImplemented) *

Підсумок

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

* Мультисети можуть бути опосередковано емульовані collections.Counter(), подібно подібним відображенням множин (лічильників).


4

Як позначено в інших відповідях, набори - це структури даних (і математичні поняття), які не зберігають порядок елементів -

Однак, використовуючи комбінацію наборів і словників, можливо, ви можете досягти бажаного користувача - спробуйте скористатися цими фрагментами:

# 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

1

Спираючись на відповідь Свена, я знайшов, використовуючи колекції. Упорядкований малюнок на зразок цього допоміг мені здійснити те, що ви хочете, і дозвольте мені додати більше елементів до дикту:

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 () на dict і отримати набір:

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

вам потрібно зробити, list(z.keys())щоб отримати вихідний список.
jxn

в Python 3, так. не в Python 2, хоча я мав би вказати.
Джим

0

Впровадження концепції з найвищим балом вище, яка повертає її до списку:

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

Тестували (коротко) на Python 3.6 та Python 2.7.


0

Якщо у ваших двох початкових списках є невелика кількість елементів, на яких ви хочете виконати операцію встановлення різниці, замість того, щоб використовувати, collections.OrderedDictщо ускладнює реалізацію і робить її менш читаною, ви можете використовувати:

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

Його складність у часі не така гарна, але вона акуратна і проста для читання.


0

Цікаво, що люди завжди використовують «проблему реального світу», щоб пожартувати над визначенням у теоретичній науці.

Якщо встановлено порядок, спочатку потрібно розібратися в наступних проблемах. Якщо у вашому списку є дублюючі елементи, яким має бути порядок, коли ви перетворюєте його на набір? Який порядок, якщо ми об'єднаємо два набори? Який порядок, якщо ми перетинаємо дві множини з різним порядком на одних і тих же елементах?

Крім того, встановити набагато швидше в пошуку конкретного ключа, який дуже добре працює в наборах (і саме тому вам потрібен набір, але не список).

Якщо ви дійсно дбаєте про індекс, просто зберігайте його як список. Якщо ви все ще хочете виконати задану операцію над елементами у багатьох списках, найпростішим способом є створення словника для кожного списку з однаковими ключами в наборі разом зі значенням списку, що містить увесь індекс ключа у вихідному списку.

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}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.