Множина: типи set, frozensent

Множини в Python представлені типами set та frozenset і використовуються для збереження неупорядкованих колекцій унікальних елементів. На відміну від списків і кортежів, множина автоматично видаляє дублікати та не гарантує порядок елементів.

У цьому розділі:

Коли використовувати set?

Множини слід використовувати тоді, коли:

  • потрібно зберігати унікальні значення,
  • неважливий порядок елементів,
  • потрібно швидко перевірити наявність елемента (x in my_set),
  • потрібно прибрати дублікати зі списку,
  • ви виконуєте математичні операції над множинами: об'єднання, перетин, різницю тощо.

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

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

Множина задається за допомогою фігурних дужок {...} або функції set():

s = {1, 2, 3}
print(s)  # {1, 2, 3}

empty = set()  # порожня множина
print(type(empty))  # <class 'set'>

⚠️ Увага! {} — це порожній словник (тип dict), а не множина. Щоб створити порожню множину, використовуйте set().

v1 = {}
print(type(v1)) # <class 'dict'>

Python автоматично видаляє дублікати:

a = {1, 2, 2, 3, 3, 3}
print(a)  # {1, 2, 3}

Можна створити множину з інших типів, Python також автоматично прибере дублікати:

chars = set("hello")
print(chars)  # {'h', 'e', 'l', 'o'} — `l` лише один раз.
s1 = set([1, 2, 2, 3, 3, 3])
print(s1) # {1, 2, 3}
s2 = set([1, 2, 2, 3, 3, 3])
print(s2) # {1, 2, 3}

🤔 Чому set("hello") повертає елементи у різному порядку?

Множина (set) у Python — це неупорядкована структура даних. Це означає, що порядок елементів у множині не визначається та не гарантується. Коли ви викликаєте set("hello"), Python створює множину унікальних символів: {'h', 'e', 'l', 'o'}. Проте порядок виводу цих символів при друці залежить від внутрішньої хеш-таблиці, яку Python використовує для зберігання множин.

Навіть якщо вміст той самий, порядок може змінюватися:

print(set("hello"))  # {'h', 'e', 'l', 'o'}
print(set("hello"))  # {'o', 'h', 'l', 'e'}

Це не помилка і не випадковість — це особливість множин у Python. Якщо вам важливо зберегти порядок, використовуйте список або кортеж (які зберігають порядок елементів), або перетворіть множину у список та відсортуйте:

print(sorted(set("hello")))  # ['e', 'h', 'l', 'o']

Таким чином, set("hello") завжди міститиме ті самі символи, але їх порядок при виведенні може змінюватися — як при перегляді вмісту коробки, у якій елементи перемішуються при кожному відкритті.

Основні операції

Уявіть, що s = {1, 2, 3}:

Операція Опис Приклад Результат
len() Кількість елементів у множині, повертається ціле len({1, 2, 3}) 3
in Перевірка наявності елемента, повертається значення типу bool 2 in {1, 2, 3} True
add(x) Додає значення x до множини (обʼєкт множини змінюється в памʼяті) s.add(5) {1, 2, 3, 5}
remove(x) Видаляє значення x, якщо існує, інакше — помилка. Обʼєкт множини змінюється в памʼяті. s.remove(2) {1, 3}
discard(x) Видаляє значення x, якщо існує (без помилки). Обʼєкт множини змінюється в памʼяті. s.discard(100) Без змін
clear() Очищає множину. Обʼєкт множини змінюється в памʼяті. s.clear() set()
s = {1, 2, 3}
s.add(4)
s.remove(2)
print(s)  # {1, 3, 4}

Python пропонує два способи видалити елемент з множини: методи remove() та discard(). Якщо ви спробуєте видалити елемент, якого немає у множині, за допомогою remove(), виникне помилка KeyError, і виконання програми буде перерване:

s = {1, 2, 3}
s.remove(10)  # KeyError: 10
print(s)

Натомість метод discard() працює безпечно: якщо елемент у множині є — він буде видалений, а якщо немає — Python просто проігнорує запит без помилки:

s = {1, 2, 3}
s.discard(10)  # Нічого не станеться, помилки немає
print(s)

Тобто discard() — це мʼякий спосіб видалення, а remove() — суворий. Уявіть, що remove() — це вимогливий охоронець: якщо шуканого немає, він здіймає тривогу. А discard() — спокійний працівник: не знайшов — просто пішов далі.

Множинні операції

Множини особливо корисні завдяки підтримці математичних операцій над множинами:

Операція Python-синтаксис Опис Приклад Результат
Обʼєднання a | b або a.union(b) Елементи, які є в a або b {1, 2} | {2, 3} {1, 2, 3}
Перетин a & b або a.intersection(b) Елементи, які є і в a, і в b {1, 2} & {2, 3} {2}
Різниця a - b або a.difference(b) Елементи, які є в a, але не в b {1, 2, 3} - {2} {1, 3}
Симетрична різниця a ^ b або a.symmetric_difference(b) Елементи, які є в a або b, але не в обох {1, 2} ^ {2, 3} {1, 3}
a = {1, 2, 3}
b = {2, 3, 4}
print(a | b)  # {1, 2, 3, 4}
print(a & b)  # {2, 3}
print(a - b)  # {1}
print(a ^ b)  # {1, 4}

Ці операції не змінюють оригінальні множини — вони повертають нову множину.

Порівняння множин

Усі операції повертають значення типу bool: True або False:

Операція Опис Приклад Результат
a <= b a є підмножиною b {1, 2} <= {1, 2, 3} True
a < b a — істинна підмножина b {1, 2} < {1, 2, 3} True
a >= b a містить b {1, 2, 3} >= {2} True
a == b множини однакові за вмістом {1, 2} == {2, 1} True

Конвертація у list або tuple

Якщо вам потрібна впорядкованість — конвертуйте множину у список або кортеж:

s = {3, 1, 2}
lst = list(s)
tpl = tuple(s)
print(lst)  # [1, 2, 3] (порядок не гарантується)

❗Увага! Порядок елементів у множині випадковий, тому list(set(...)) часто використовують для видалення дублікатів зі списку, не зберігаючи порядок.

Множини та зміна

Множини — змінюваний тип (mutable), тому можна додавати або видаляти елементи. Але є також незмінний варіант множини — frozenset. Він поводиться як звичайна множина, але не підтримує методи add(), remove() тощо.

fs = frozenset([1, 2, 3])
print(2 in fs)  # True
fs.add(4)       # AttributeError

Практичне завдання: пошук унікальних та спільних користувачів

У нас є два списки користувачів:

  • registered_users — список усіх зареєстрованих користувачів на сайті.
  • active_users — список користувачів, які сьогодні заходили на сайт.

Завдання:

  1. Виведіть користувачів, які сьогодні були активні (спільні для обох списків).
  2. Виведіть користувачів, які зареєстровані, але сьогодні не заходили.
  3. Виведіть користувачів, які сьогодні заходили, але не зареєстровані (можливо, гість або бот).
  4. Виведіть всі унікальні імена користувачів, які згадуються у будь-якому зі списків.

Рішення:

registered_users = {"anna", "olena", "ivan", "petro", "serhii"}
active_users = {"ivan", "serhii", "bot42", "olena"}

# 1. Спільні користувачі (і зареєстровані, і активні)
both = registered_users & active_users
print("Активні зареєстровані:", both, len(both))

# 2. Зареєстровані, але сьогодні не заходили
inactive = registered_users - active_users
print("Зареєстровані, але неактивні:", inactive, len(inactive))

# 3. Активні, але не зареєстровані
unregistered = active_users - registered_users
print("Активні незареєстровані:", unregistered, len(unregistered))

# 4. Усі унікальні згадані імена
all_users = registered_users | active_users
print("Усі унікальні користувачі:", all_users, len(all_users))