Множини (Sets)¶
Довідник. Ноутбук уроку, де множини з'являються вперше в контексті задачі (унікальні офіціанти в системі чайових), — Урок 5.
Множина (set) у Python — це неупорядкована колекція унікальних об'єктів. Вона працює так само, як математичні множини, і підтримує відповідні операції (перетин, об'єднання тощо).
Множину можна уявити як мішок, у який закидають речі:
- Невідомо, в якому порядку вони там лежать.
- Якщо покласти туди два однакових яблука, "мішок" залишить лише одне — усі елементи абсолютно унікальні.
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)
Результат:
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}")
Результат:
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]} км")
Результат:
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 · Словники