2.10. Множини

Множина — це невпорядкована колекція унікальних елементів. Додавання вже наявного значення не дає жодного ефекту; ітерація повертає кожне значення рівно один раз. Множини — правильний інструмент, коли важлива перевірка наявності та усунення дублікатів, а порядок — ні.

2.10.1. Створення множини

Використовуйте фігурні дужки для непорожньої множини або set() для порожньої:

colours = {"red", "green", "blue"}
empty = set()

Фігурні дужки схожі на літерал dict; {} само по собі є порожнім dict, а не порожньою множиною — один із класичних казусів Python. Використовуйте set() для порожнього випадку.

set() також будує множину з будь-якого ітерованого об’єкта — це стандартний спосіб видалити дублікати з послідовності:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

Output:

{1, 2, 3, 4}

Порядок виведення може відрізнятися — множини не гарантують жодного конкретного порядку ітерації.

2.10.2. Множина vs словник

Множини та словники зберігають унікальні елементи в хеш-таблиці. Різниця в тому, що кожен елемент несе з собою:

  • dict зберігає пари ключ-значення. Пошук за ключем повертає його значення.

  • set зберігає лише елементи. Пошук елемента повідомляє лише про те, чи є він там.

Вибір між ними залежить від того, чи має значення дані поруч із кожним елементом:

  • Обирайте множину, коли жодне значення не потрібно поруч із кожним елементом — вам важливо лише, чи присутній елемент, або ви комбінуєте групи унікальних елементів за допомогою об’єднання / перетину.

  • Обирайте словник, коли кожен елемент пов’язаний з даними, які потрібно отримати при пошуку — конфігурація, кеш, лічильник за ключем-іменем.

Обидва типи мають схожий синтаксис, що й є основним джерелом плутанини. Відмінності в одному блоці:

set

dict

зберігає

унікальні елементи

унікальні ключі, кожен з відповідним значенням

непорожній літерал

{1, 2, 3}

{"a": 1, "b": 2}

порожній літерал

set()

{}

перевірка наявності

x in s

k in d (лише ключі)

отримати значення

n/a

d[k]

додати елемент

s.add(x)

d[k] = v

ітерувати

повертає елементи

повертає ключі (використовуйте d.items() для пар)

Асиметрія між непорожнім і порожнім літералами — ось на що варто звернути увагу:

  • Фігурні дужки з елементами всередині{1, 2, 3} — є літералом множини; фігурні дужки з парами ключ-значення{"a": 1} — є літералом словника. Парсер розрізняє їх за вмістом.

  • Фігурні дужки з нічим всередині{} — є порожнім словником, а не порожньою множиною. Словники з’явилися першими; порожній літерал належить їм. Порожня множина взагалі не має літерала у фігурних дужках і завжди записується як set().

Поширений шаблон: якщо зі словника читаються лише ключі, варто перейти до множини — це робить намір очевидним і прибирає невикористані значення з пам’яті.

2.10.3. Додавання та видалення

  • set.add() — вставляє один елемент.

  • set.discard() — видаляє елемент, якщо він присутній, нічого не робить, якщо його немає.

  • set.remove() — видаляє елемент; викидає KeyError, якщо він відсутній.

  • set.clear() — очищає множину.

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

Output:

{1, 3, 4}

2.10.4. Перевірка наявності

Оператор in перевіряє наявність елемента. На множині це займає приблизно сталий час незалежно від розміру — саме тому варто обирати множину замість list, коли потрібно лише відповісти на питання «чи є це значення там»:

if "red" in colours:
    print("colour is allowed")

list з тим самим вмістом щоразу сканувала б від початку — це нормально для десяти елементів, але повільно для десяти тисяч.

2.10.5. Операції з множинами

Дві множини можна комбінувати за допомогою стандартних математичних операцій. Кожна має форму з оператором і форму з методом:

  • a | b або a.union(b) — все, що є в будь-якій із множин.

  • a & b або a.intersection(b) — лише те, що є в обох.

  • a - b або a.difference(b) — є в a, але не в b.

  • a ^ b або a.symmetric_difference(b) — є в одній, але не в обох.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

Output:

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

Форми з операторами лише для читання; форми з методами приймають будь-який ітерований об’єкт справа, а не лише іншу множину (a.union([5, 6])). Обирайте ту, яка краще читається в контексті.

2.10.6. Що може входити до множини

Елементи множини мають бути хешованими — те саме обмеження, що й для ключів dict. int, float, str, bool, bytes і tuple (якщо їхній вміст також хешований) — все це підходить. list і dict — ні; спроба додати їх викидає TypeError.

2.10.7. frozenset

Звичайна set є змінюваною: кожен виклик add / remove / discard змінює об’єкт на місці. Ця змінюваність позбавляє її можливості бути хешованою, тому множину не можна використовувати як ключ dict або як елемент іншої множини.

frozenset — це незмінний аналог. Він має ті самі операції пошуку та оператори (in, |, &, -, ^), що й set, але не має add / remove і жодних методів, що змінюють вміст. Оскільки вміст frozenset ніколи не змінюється, його хеш добре визначений — тобто він є хешованим:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

Output:

RGB

Конструюйте frozenset з будь-якого ітерованого об’єкта — frozenset() для порожнього випадку, frozenset(some_set) для створення незмінного знімку існуючої множини:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

Дві поширені причини звернутися до нього:

  • Використання як ключа словника або елемента множини. Коли одне значення не може відобразити те, що вам потрібно, frozenset значень може — «множина ознак, підтримуваних цим драйвером», «множина виводів, які використовує цей профіль».

  • Зафіксувати константу. frozenset на рівні модуля з допустимими іменами не може бути випадково змінений кодом, що його викликає; звичайна set — може. Надавайте перевагу frozenset для всього, що має бути доступним лише для читання після створення.