Перейти до змісту

Множини (Sets)

Довідник. Ноутбук уроку, де множини з'являються вперше в контексті задачі (унікальні офіціанти в системі чайових), — Урок 5.

Множина (set) у Python — це неупорядкована колекція унікальних об'єктів. Вона працює так само, як математичні множини, і підтримує відповідні операції (перетин, об'єднання тощо).

Множину можна уявити як мішок, у який закидають речі:

  1. Невідомо, в якому порядку вони там лежать.
  2. Якщо покласти туди два однакових яблука, "мішок" залишить лише одне — усі елементи абсолютно унікальні.

1. Базові принципи та створення

  • Швидкість: перевірка, чи є елемент у множині (x in my_set), працює миттєво (O(1)) завдяки хеш-таблицям під капотом.
  • Елементи: повинні бути незмінними (можна додати числа або рядки, але не можна додати список або словник).
# 1. Створення множини
fruits = {"яблуко", "банан", "апельсин"}
print("Початкова множина:", fruits)

# 2. ⚠️ Важливий нюанс для новачків: порожня множина
# Щоб створити порожню множину, треба set(), а не фігурні дужки!
empty_dict = {}     # це створить порожній словник
empty_set = set()   # це створить порожню множину

print("\nТип empty_dict:", type(empty_dict))
print("Тип empty_set:", type(empty_set))

# 3. Швидке видалення дублікатів зі списку
dirty_data = [1, 2, 2, 3, 3, 3, 4, 5, 5]
clean_data = list(set(dirty_data))
print("\nОчищений список від дублікатів:", clean_data)

Результат:

Початкова множина: {'банан', 'апельсин', 'яблуко'}

Тип empty_dict: <class 'dict'>
Тип empty_set: <class 'set'>

Очищений список від дублікатів: [1, 2, 3, 4, 5]

Порядок елементів множини не гарантований

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

2. Додавання та видалення елементів

Множини — змінювані об'єкти, тому їхнім вмістом можна керувати після створення.

Метод Опис
add(x) Додає один елемент x. Якщо він вже існує, метод нічого не робить.
remove(x) Видаляє елемент x. Увага: якщо елемента немає — KeyError.
discard(x) "Безпечно" видаляє елемент x — якщо його немає, просто ігнорує команду без помилок.
pop() Видаляє та повертає один довільний елемент. На порожній множині — KeyError.
clear() Повністю очищає множину, залишаючи set().
# Управління активними сесіями користувачів
active_users = {"admin", "moderator", "guest"}

# Додавання
active_users.add("new_user")
active_users.add("admin")  # нічого не зміниться, дублікат ігнорується
print("Після додавання:", active_users)

# Видалення
active_users.remove("guest")

# Безпечне видалення (користувача 'hacker' немає, але discard не зламає програму)
active_users.discard("hacker")
print("Після видалення:", active_users)

# Вилучення довільного елемента
random_user = active_users.pop()
print(f"Вилучено користувача: {random_user}")
print("Залишилось:", active_users)

# Очищення
active_users.clear()
print("Після clear():", active_users)

Результат (реальний прогін — конкретний "довільний" елемент у pop() може відрізнятись у вас):

Після додавання: {'moderator', 'admin', 'new_user', 'guest'}
Після видалення: {'moderator', 'admin', 'new_user'}
Вилучено користувача: moderator
Залишилось: {'admin', 'new_user'}
Після clear(): set()

3. Математичні операції (створення нових множин)

Ці методи реалізують класичну теорію множин. Вони не змінюють оригінальні множини, а створюють нову.

Важливий нюанс: методи (наприклад, .union()) можуть приймати будь-який ітерований об'єкт (список, рядок). Оператори (наприклад, |) вимагають, щоб з обох боків були саме множини.

Операція Метод Оператор Результат
Об'єднання (Union) union(other) \| усі унікальні елементи з обох множин
Перетин (Intersection) intersection(other) & елементи, які є одночасно в обох множинах
Різниця (Difference) difference(other) - елементи першої множини, яких немає у другій
Симетрична різниця (Symmetric Difference) symmetric_difference(other) ^ елементи, які є в будь-якій множині, але не в обох одночасно
# --- ПРИКЛАД: соціальна мережа та задачі ---
set_A = {"Антон", "Максим", "Юлія"}
set_B = {"Максим", "Світлана"}

print("Об'єднання (|):", set_A | set_B)   # всі унікальні люди
print("Перетин (&):", set_A & set_B)       # спільні (тільки Максим)
print("Різниця (A - B):", set_A - set_B)   # є в A, але немає в B (Антон, Юлія)
print("Сим. різниця (^):", set_A ^ set_B)  # всі, окрім Максима

# Демонстрація гнучкості методів (передаємо список у метод union)
new_people_list = ["Олег", "Марія", "Антон"]
expanded_set = set_A.union(new_people_list)
print("\nМетод union() зі списком:", expanded_set)

Результат:

Об'єднання (|): {'Юлія', 'Антон', 'Світлана', 'Максим'}
Перетин (&): {'Максим'}
Різниця (A - B): {'Юлія', 'Антон'}
Сим. різниця (^): {'Юлія', 'Світлана', 'Антон'}

Метод union() зі списком: {'Марія', 'Олег', 'Юлія', 'Антон', 'Максим'}

4. Оновлення множини "на місці" (мутуючі операції)

Якщо множини дуже великі, створювати нові об'єкти в пам'яті неефективно. Ці методи записують результат прямо в поточну множину:

  • update(other) або |= — об'єднання на місці (додає всі елементи з other);
  • intersection_update(other) або &= — залишає лише спільні елементи;
  • difference_update(other) або -= — видаляє елементи, присутні в other;
  • symmetric_difference_update(other) або ^= — записує симетричну різницю.
# Збираємо унікальні IP-адреси з логів
seen_ips = {"192.168.1.1", "10.0.0.5"}
new_logs_batch = ["10.0.0.5", "172.16.0.2", "192.168.1.1"]

# update() ефективно додасть нові IP без створення нової множини в пам'яті
seen_ips.update(new_logs_batch)
print("Оновлені IP-адреси:", seen_ips)

# difference_update() — видаляємо внутрішні IP, залишаємо лише зовнішні
internal_ips = {"192.168.1.1", "10.0.0.5"}
seen_ips.difference_update(internal_ips)
print("Тільки зовнішні IP:", seen_ips)

Результат:

Оновлені IP-адреси: {'10.0.0.5', '192.168.1.1', '172.16.0.2'}
Тільки зовнішні IP: {'172.16.0.2'}

5. Логічні перевірки (відношення між множинами)

Ці методи завжди повертають True або False і корисні для валідації даних:

  • issubset(other) або <= — чи всі елементи першої множини містяться у другій (підмножина);
  • issuperset(other) або >= — чи містить перша множина всі елементи іншої (надмножина);
  • isdisjoint(other) — чи правда, що множини взагалі не мають спільних елементів.
# ПРИКЛАД: валідація колонок у таблиці даних
required_columns = {"id", "timestamp", "value"}
provided_columns = {"id", "timestamp", "value", "sensor_name", "status"}

# Перевіряємо, чи всі обов'язкові колонки присутні у наданих
is_valid = required_columns.issubset(provided_columns)
print(f"Чи валідна структура даних? {is_valid}")

# Перевірка на конфлікт інтересів (isdisjoint)
# Якщо перетин порожній — поверне True (спільних проектів немає)
frontend_tasks = {"ui_button", "css_fix"}
backend_tasks = {"api_endpoint", "db_migration"}
no_overlap = frontend_tasks.isdisjoint(backend_tasks)
print(f"Чи незалежні команди? {no_overlap}")

Результат:

Чи валідна структура даних? True
Чи незалежні команди? True

6. Інші корисні операції (len, copy, frozenset)

  • len(s) — вбудована функція, кількість елементів;
  • copy() — створює поверхневу копію (shallow copy) множини;
  • frozenset() — створює незмінну версію множини. Звичайну set не можна використати як ключ у словнику (бо вона змінювана); frozenset вирішує цю проблему, бо статичний і хешований.
my_set = {10, 20, 30}
print(f"Розмір множини: {len(my_set)} елементів")

set_copy = my_set.copy()

# Використання frozenset як ключа у словнику кешування
# Порядок міст у frozenset не має значення! Маршрут знайдеться незалежно від порядку.
routes_cache = {
    frozenset(["Київ", "Чернівці"]): 540,
    frozenset(["Львів", "Чернівці"]): 270,
}

search_key = frozenset(["Чернівці", "Київ"])
print(f"\nВідстань: {routes_cache[search_key]} км")

Результат:

Розмір множини: 3 елементів

Відстань: 540 км

7. Просунуті приклади використання (для інженерії даних та ML)

Реальні сценарії, де множини демонструють максимальну ефективність: оптимізація ETL-пайплайнів та підготовка даних для навчання нейромереж.

import time

# --- ПРИКЛАД 1: оптимізація швидкості пошуку в ETL-пайплайні ---
# Збираємо гідрологічні дані з датчиків для проєкту GeoHydroAI.
processed_ids_list = list(range(10_000_000))  # 10 млн записів
processed_ids_set = set(processed_ids_list)
new_sensor_id = 9_999_999

# Пошук у списку O(N)
start_time = time.time()
_ = new_sensor_id in processed_ids_list
list_time = time.time() - start_time

# Пошук у множині O(1)
start_time = time.time()
_ = new_sensor_id in processed_ids_set
set_time = time.time() - start_time

print(f"Час пошуку в List: {list_time:.5f} сек")
print(f"Час пошуку в Set:  {set_time:.5f} сек")
print(f"Множина швидша приблизно у {list_time / (set_time + 1e-9):,.0f} разів!\n")

# --- ПРИКЛАД 2: валідація токенів під час підготовки LLM ---
corpus_base = "штучний інтелект аналізує гідрологічні дані"
corpus_new = "гідрологічні дані важливі для нейромереж"

vocab_base = set(corpus_base.split())
vocab_new = set(corpus_new.split())

# Які нові терміни з'явилися у новому датасеті? (різниця)
new_unique_tokens = vocab_new - vocab_base
print(f"Нові токени для донавчання: {new_unique_tokens}")

Результат (реальний прогін; точні секунди залежать від заліза, порядок величини — суть):

Час пошуку в List: 0.05823 сек
Час пошуку в Set:  0.00000 сек
Множина швидша приблизно у 24,413 разів!

Нові токени для донавчання: {'для', 'нейромереж', 'важливі'}


Дивись також: Списки · Кортежі та namedtuple · Словники