17. Множества: работа с уникальными неупорядоченными данными
В предыдущих главах мы работали со списками(list) (упорядоченными, изменяемыми коллекциями) и словарями(dictionary) (отображениями «ключ-значение»). Теперь мы рассмотрим множества(sets) — тип коллекции Python, специально предназначенный для хранения уникальных элементов и эффективного выполнения математических операций над множествами.
Множества особенно полезны, когда нужно устранить дубликаты, быстро проверить принадлежность элемента или выполнить операции вроде поиска общих элементов между коллекциями. В отличие от списков, множества неупорядочены и не могут содержать повторяющиеся значения — попытка добавить один и тот же элемент дважды не даёт эффекта.
17.1) Создание множеств и базовые операции
17.1.1) Создание множеств с фигурными скобками
Самый распространённый способ создать множество — использовать фигурные скобки {} со значениями, разделёнными запятыми:
# Создание множества языков программирования
languages = {"Python", "JavaScript", "Java", "C++"}
print(languages) # Output: {'Python', 'JavaScript', 'Java', 'C++'}
print(type(languages)) # Output: <class 'set'>Важно: порядок элементов при выводе множества может отличаться от порядка, в котором вы их ввели. Множества — это неупорядоченные коллекции, то есть Python не поддерживает какой-либо определённой последовательности:
numbers = {5, 2, 8, 1, 9}
print(numbers) # Output might be: {1, 2, 5, 8, 9} or another orderПорядок вывода может отличаться между запусками Python и версиями. Никогда не полагайтесь на то, что множества сохраняют конкретный порядок — если порядок важен, используйте список(list).
17.1.2) Множества автоматически удаляют дубликаты
Одно из самых полезных свойств множеств — то, что они автоматически устраняют дубликаты. Если вы пытаетесь создать множество с повторяющимися элементами, сохраняется только по одной копии каждого уникального значения:
# Создание множества с повторяющимися значениями
student_ids = {101, 102, 103, 102, 101, 104}
print(student_ids) # Output: {101, 102, 103, 104}
# Это свойство делает множества идеальными для удаления дубликатов
grades = [85, 90, 85, 78, 90, 92, 78, 85]
unique_grades = set(grades)
print(unique_grades) # Output: {78, 85, 90, 92}Эта автоматическая дедупликация происходит потому, что множества используют математическую модель множеств, где каждый элемент может встречаться только один раз. Когда вы добавляете значение, которое уже существует, множество просто игнорирует дубликат.
17.1.3) Создание множеств с помощью конструктора set()
Вы можете создавать множества из других итерируемых объектов с помощью конструктора set(). Это особенно полезно для преобразования списков(list), кортежей(tuple) или строк в множества:
# Создание множества из списка
colors_list = ["red", "blue", "green", "red", "yellow"]
colors_set = set(colors_list)
print(colors_set) # Output: {'red', 'blue', 'green', 'yellow'}
# Создание множества из строки (каждый символ становится элементом)
letters = set("programming")
print(letters) # Output: {'p', 'r', 'o', 'g', 'a', 'm', 'i', 'n'}
# Создание множества из кортежа
coordinates = set((10, 20, 30, 20, 10))
print(coordinates) # Output: {10, 20, 30}Когда вы создаёте множество из строки, каждый уникальный символ становится отдельным элементом. Это полезно, чтобы найти все различные символы в тексте:
text = "Mississippi"
unique_chars = set(text.lower())
print(unique_chars) # Output: {'m', 'i', 's', 'p'}
print(f"The word contains {len(unique_chars)} unique letters")
# Output: The word contains 4 unique letters17.1.4) Создание пустого множества
Вот важный подводный камень: вы не можете создать пустое множество с помощью {}, потому что Python интерпретирует это как пустой словарь(dictionary). Вместо этого нужно использовать set():
# WRONG - это создаёт пустой словарь, а не множество
empty_dict = {}
print(type(empty_dict)) # Output: <class 'dict'>
# CORRECT - это создаёт пустое множество
empty_set = set()
print(type(empty_set)) # Output: <class 'set'>
print(empty_set) # Output: set()Это различие существует потому, что словари были добавлены в Python раньше множеств, так что {} уже было занято для пустых словарей. Когда вы печатаете пустое множество, Python отображает его как set(), чтобы избежать путаницы.
Распространённая путаница у новичков: при создании множества из одного элемента с использованием переменной множество содержит значение переменной, а не имя переменной:
# Понимание создания множества с переменными
x = 5
my_set = {x} # Creates {5}, not {'x'}
print(my_set) # Output: {5}
# Если вы хотите множество, содержащее строку 'x':
my_set = {'x'}
print(my_set) # Output: {'x'}
# Это относится к любому выражению
result = 10 + 5
my_set = {result} # Creates {15}
print(my_set) # Output: {15}17.1.5) Базовые свойства и операции множеств
Множества поддерживают несколько фундаментальных операций, которые делают их полезными для обработки данных:
# Проверка количества уникальных элементов
website_visitors = {"alice", "bob", "charlie", "alice", "david"}
print(f"Unique visitors: {len(website_visitors)}")
# Output: Unique visitors: 4
# Проверка принадлежности с помощью 'in' (для множеств очень быстро)
if "alice" in website_visitors:
print("Alice visited the website")
# Output: Alice visited the website
# Проверка непринадлежности
if "eve" not in website_visitors:
print("Eve has not visited yet")
# Output: Eve has not visited yetПроверка принадлежности с помощью in — одно из ключевых преимуществ множеств. Для больших коллекций проверка, существует ли элемент в множестве, выполняется намного быстрее, чем в списке. Почему это важно, мы рассмотрим в разделе 17.5.
17.2) Добавление и удаление элементов из множеств
В отличие от кортежей(tuple) (которые неизменяемы), множества изменяемы — вы можете добавлять и удалять элементы после создания. Однако сами элементы должны быть неизменяемых типов (мы рассмотрим это ограничение в разделе 17.7).
17.2.1) Добавление одиночных элементов с add()
Добавлять отдельные элементы в множество просто с помощью метода add(). Если элемент уже существует, множество остаётся неизменным — ошибка не возникает и дубликат не создаётся:
# Формирование множества выполненных задач
completed_tasks = {"task1", "task2"}
print(completed_tasks) # Output: {'task1', 'task2'}
# Добавление новой задачи
completed_tasks.add("task3")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}
# Добавление дубликата ни на что не влияет
completed_tasks.add("task1")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}Такое поведение делает множества идеальными для отслеживания уникальных появлений. Вы можете безопасно вызывать add() без проверки, существует ли элемент — множество автоматически обрабатывает дубликаты.
17.2.2) Добавление нескольких элементов с update()
Чтобы добавить сразу несколько элементов, используйте update(), который принимает любой итерируемый объект (список(list), кортеж(tuple), другое множество и т. д.) и добавляет все его элементы в множество:
# Начинаем с небольшого множества навыков
skills = {"Python", "SQL"}
print(skills) # Output: {'Python', 'SQL'}
# Добавление нескольких навыков из списка
new_skills = ["JavaScript", "Docker", "Python"]
skills.update(new_skills)
print(skills) # Output: {'Python', 'SQL', 'JavaScript', 'Docker'}Обратите внимание: "Python" присутствовал и в исходном множестве, и в добавляемом списке, но множество всё равно содержит только одну копию. Метод update() может принимать несколько итерируемых объектов в качестве аргументов:
# Объединение навыков из нескольких источников
current_skills = {"Python"}
course_skills = ["JavaScript", "HTML"]
job_requirements = {"SQL", "Python", "Docker"}
current_skills.update(course_skills, job_requirements)
print(current_skills)
# Output: {'Python', 'JavaScript', 'HTML', 'SQL', 'Docker'}17.2.3) Удаление элементов с remove()
Удаление элементов требует осторожности. Метод remove() удаляет элемент из множества, но вызывает KeyError, если элемента не существует:
# Управление активными пользователями
active_users = {"alice", "bob", "charlie", "david"}
# Удаление пользователя, который вышел из системы
active_users.remove("bob")
print(active_users) # Output: {'alice', 'charlie', 'david'}
# Попытка удалить несуществующий элемент вызывает ошибку
# active_users.remove("eve") # Raises: KeyError: 'eve'Поскольку remove() вызывает ошибку для отсутствующих элементов, лучше использовать его, когда вы уверены, что элемент существует, или когда хотите поймать ошибку, если его нет:
# Безопасное удаление с обработкой ошибок (мы подробнее изучим try/except в главе 28)
users = {"alice", "bob", "charlie"}
user_to_remove = "david"
if user_to_remove in users:
users.remove(user_to_remove)
print(f"Removed {user_to_remove}")
else:
print(f"{user_to_remove} was not in the set")
# Output: david was not in the set17.2.4) Безопасное удаление элементов с discard()
Для более безопасного удаления элементов, которое не будет вызывать ошибки, discard() предоставляет «прощающее» альтернативное решение. Он удаляет элемент, если он присутствует, но ничего не делает, если элемента нет:
# Управление корзиной покупок
cart_items = {"apple", "banana", "orange"}
# Безопасное удаление товаров (нет ошибки, если товара не существует)
cart_items.discard("banana")
print(cart_items) # Output: {'apple', 'orange'}
cart_items.discard("grape") # Ошибки не будет, даже если 'grape' нет в множестве
print(cart_items) # Output: {'apple', 'orange'}Используйте discard(), когда хотите гарантировать, что элемента нет в множестве, независимо от того, был ли он там изначально. Используйте remove(), когда отсутствие элемента означает ошибочное состояние, которое вы хотите отловить.
17.2.5) Удаление и возврат произвольного элемента с pop()
Метод pop() удаляет и возвращает произвольный элемент множества. Поскольку множества неупорядочены, вы не можете предсказать, какой элемент будет удалён:
# Обработка очереди ожидающих задач (порядок не важен)
pending_tasks = {"email", "report", "meeting", "review"}
# Обрабатываем одну задачу (неважно, какую именно)
task = pending_tasks.pop()
print(f"Processing: {task}") # Output: Processing: email (or another task)
print(f"Remaining: {pending_tasks}")
# Output: Remaining: {'report', 'meeting', 'review'} (without the popped task)Если вызвать pop() для пустого множества, будет KeyError:
empty_set = set()
# empty_set.pop() # Raises: KeyError: 'pop from an empty set'Метод pop() полезен, когда нужно обработать все элементы множества, но порядок не важен:
# Обработка всех элементов множества
items_to_process = {"item1", "item2", "item3"}
while items_to_process:
item = items_to_process.pop()
print(f"Processing {item}")
# Обработка элемента...
print("All items processed")
# Output:
# Processing item1
# Processing item2
# Processing item3
# All items processed17.2.6) Удаление всех элементов с clear()
Метод clear() удаляет все элементы из множества, оставляя его пустым:
# Сброс данных сессии
session_data = {"user_id", "timestamp", "ip_address"}
print(session_data) # Output: {'user_id', 'timestamp', 'ip_address'}
session_data.clear()
print(session_data) # Output: set()
print(len(session_data)) # Output: 0Это эффективнее, чем создавать новое пустое множество, если вы хотите переиспользовать тот же объект множества.
17.3) Операции над множествами: объединение, пересечение, разность и симметрическая разность
Множества поддерживают математические операции, которые позволяют эффективно объединять, сравнивать и анализировать коллекции. Эти операции фундаментальны для теории множеств и имеют множество практических применений в обработке данных.
17.3.1) Объединение: комбинирование множеств
Начнём с практического сценария, чтобы понять, почему объединение важно. Представьте, что вы управляете списками студентов, записанных на разные курсы:
# Студенты, записанные на разные курсы
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# Поиск всех студентов, посещающих любой из курсов (или оба)
all_students = python_students | javascript_students
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}Объединение(union) двух множеств содержит все элементы, которые присутствуют в любом из множеств (или в обоих). Python предоставляет два способа вычислять объединение: оператор | (показан выше) и метод union():
# Тот же результат с использованием метода union()
all_students = python_students.union(javascript_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}Метод union() может принимать несколько множеств в качестве аргументов, что удобно для объединения данных из многих источников:
# Студенты на трёх разных курсах
python_students = {"alice", "bob"}
javascript_students = {"bob", "charlie"}
sql_students = {"charlie", "david"}
# Все студенты по всем курсам
all_students = python_students.union(javascript_students, sql_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david'}Ещё один пример объединения — объединение email-списков разных отделов:
# Объединение списков email из разных отделов
marketing_contacts = {"alice@company.com", "bob@company.com"}
sales_contacts = {"bob@company.com", "charlie@company.com"}
support_contacts = {"david@company.com", "alice@company.com"}
# Все уникальные контакты по отделам
all_contacts = marketing_contacts | sales_contacts | support_contacts
print(f"Total unique contacts: {len(all_contacts)}")
# Output: Total unique contacts: 417.3.2) Пересечение: поиск общих элементов
Понимание того, какие элементы встречаются в нескольких множествах, критически важно для многих задач анализа данных. Операция пересечения(intersection) отвечает на вопрос: «Что общего у этих множеств?»
# Поиск клиентов, купивших оба продукта
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# Клиенты, купившие оба продукта
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}Пересечение содержит только элементы, которые присутствуют в обоих множествах. Также можно использовать метод intersection(), который принимает несколько множеств:
# Поиск студентов, записанных на все три курса
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "charlie", "david"}
sql_students = {"charlie", "eve", "bob"}
# Студенты, посещающие все три курса
all_three = python_students.intersection(javascript_students, sql_students)
print(all_three) # Output: {'bob', 'charlie'}Вот практический пример поиска товаров, доступных на нескольких складах:
# Поиск товаров, доступных на нескольких складах
warehouse_a = {"laptop", "mouse", "keyboard", "monitor"}
warehouse_b = {"mouse", "keyboard", "printer", "scanner"}
warehouse_c = {"keyboard", "monitor", "mouse", "desk"}
# Товары, доступные на всех складах
available_everywhere = warehouse_a & warehouse_b & warehouse_c
print(f"Available in all locations: {available_everywhere}")
# Output: Available in all locations: {'mouse', 'keyboard'}17.3.3) Разность: поиск элементов, которые есть в одном множестве, но нет в другом
Иногда нужно определить, что уникально для одной коллекции. Операция разности(difference) находит элементы, которые есть в первом множестве, но отсутствуют во втором:
# Управление инвентарём: поиск расхождений
expected_items = {"item001", "item002", "item003", "item004"}
actual_items = {"item001", "item003", "item005"}
# Товары, отсутствующие в инвентаре
missing = expected_items - actual_items
print(f"Missing items: {missing}")
# Output: Missing items: {'item002', 'item004'}
# Неожиданные товары в инвентаре
unexpected = actual_items - expected_items
print(f"Unexpected items: {unexpected}")
# Output: Unexpected items: {'item005'}Также можно использовать метод difference():
# Студенты только на курсе Python (не на JavaScript)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
python_only = python_students.difference(javascript_students)
print(python_only) # Output: {'alice', 'charlie'}Важно: операция разности не коммутативна — порядок имеет значение:
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# Студенты на Python, но не на JavaScript
python_only = python_students - javascript_students
print(f"Python only: {python_only}")
# Output: Python only: {'alice', 'charlie'}
# Студенты на JavaScript, но не на Python
javascript_only = javascript_students - python_students
print(f"JavaScript only: {javascript_only}")
# Output: JavaScript only: {'david', 'eve'}17.3.4) Симметрическая разность: элементы, которые есть в одном из множеств, но не в обоих
Симметрическая разность(symmetric difference) находит элементы, которые находятся в одном из множеств, но не в обоих одновременно. Эта операция особенно полезна для выявления изменений между двумя версиями:
# Сравнение двух версий конфигурации
old_settings = {"debug", "logging", "cache", "compression"}
new_settings = {"logging", "cache", "monitoring", "security"}
# Настройки, которые изменились (добавлены или удалены)
changes = old_settings ^ new_settings
print(f"Changed settings: {changes}")
# Output: Changed settings: {'debug', 'compression', 'monitoring', 'security'}
# Чтобы увидеть отдельно, что добавили, а что удалили:
removed = old_settings - new_settings
added = new_settings - old_settings
print(f"Removed: {removed}") # Output: Removed: {'debug', 'compression'}
print(f"Added: {added}") # Output: Added: {'monitoring', 'security'}Также можно использовать метод symmetric_difference():
# Студенты ровно на одном курсе (не на обоих)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
one_course_only = python_students.symmetric_difference(javascript_students)
print(one_course_only)
# Output: {'alice', 'charlie', 'david', 'eve'}В отличие от разности, симметрическая разность коммутативна — порядок не важен:
result1 = python_students ^ javascript_students
result2 = javascript_students ^ python_students
print(result1 == result2) # Output: True17.4) Отношения подмножества и надмножества (issubset, issuperset, isdisjoint)
Помимо объединения множеств, нам часто нужно понимать отношения между ними. Python предоставляет методы, чтобы проверить, содержится ли одно множество в другом, содержит ли оно другое, или не имеет общих элементов с другим.
17.4.1) Проверка подмножеств с issubset() и <=
Множество A является подмножеством(subset) множества B, если каждый элемент A также присутствует в B. Другими словами, B содержит все элементы A (и, возможно, дополнительные).
# Пререквизиты курса
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# Проверяем, является ли basic_skills подмножеством intermediate_skills
print(basic_skills.issubset(intermediate_skills)) # Output: True
print(basic_skills <= intermediate_skills) # Output: True (same result)Множество всегда является подмножеством самого себя:
skills = {"Python", "SQL", "JavaScript"}
print(skills.issubset(skills)) # Output: True
print(skills <= skills) # Output: TrueЕсли вы хотите проверить строгое подмножество (A — подмножество B, но не равно B), используйте оператор <:
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# Строгое подмножество: basic — подмножество intermediate И они не равны
print(basic_skills < intermediate_skills) # Output: True
# Не является строгим подмножеством самого себя (они равны)
print(basic_skills < basic_skills) # Output: FalseПрактический пример проверки подмножества — проверка прав доступа или требований:
# Система прав доступа пользователя
required_permissions = {"read", "write"}
user_permissions = {"read", "write", "delete", "admin"}
# Проверяем, есть ли у пользователя все обязательные права
if required_permissions.issubset(user_permissions):
print("Access granted")
else:
print("Access denied - missing permissions")
# Output: Access granted
# Ещё один пользователь с недостаточными правами
limited_user = {"read"}
if required_permissions.issubset(limited_user):
print("Access granted")
else:
missing = required_permissions - limited_user
print(f"Access denied - missing: {missing}")
# Output: Access denied - missing: {'write'}17.4.2) Проверка надмножеств с issuperset() и >=
Множество A является надмножеством(superset) множества B, если A содержит все элементы B. Это обратное отношение к подмножеству — если A является подмножеством B, то B является надмножеством A.
# Уровни навыков
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis", "research"}
# Проверяем, является ли advanced_skills надмножеством basic_skills
print(advanced_skills.issuperset(basic_skills)) # Output: True
print(advanced_skills >= basic_skills) # Output: True (same result)Как и для подмножеств, множество всегда является надмножеством самого себя:
skills = {"Python", "SQL"}
print(skills.issuperset(skills)) # Output: TrueДля строгого надмножества (A — надмножество B, но не равно B) используйте оператор >:
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis"}
# Строгое надмножество: advanced содержит все элементы basic И имеет больше
print(advanced_skills > basic_skills) # Output: True
# Не является строгим надмножеством самого себя
print(advanced_skills > advanced_skills) # Output: False17.4.3) Проверка на непересекающиеся множества с isdisjoint()
Два множества являются непересекающимися(disjoint), если у них нет общих элементов — их пересечение пустое. Метод isdisjoint() возвращает True, если множества не имеют общих элементов:
# Проверка конфликтов в расписании
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "art", "music"}
# Проверяем, есть ли конфликты (одинаковый предмет в обеих сессиях)
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: No scheduling conflictsКогда множества пересекаются:
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "math", "music"}
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: Conflicts: {'math'}Пустые множества непересекаются со всеми множествами (включая другие пустые множества):
empty = set()
numbers = {1, 2, 3}
print(empty.isdisjoint(numbers)) # Output: True
print(empty.isdisjoint(empty)) # Output: True17.5) Когда использовать множества вместо списков
Понимание того, когда использовать множества вместо списков, критически важно для написания эффективного Python-кода. Хотя оба типа хранят коллекции элементов, у них разные характеристики, из-за которых каждый подходит для разных задач.
17.5.1) Используйте множества для быстрой проверки принадлежности
Одно из самых значимых преимуществ множеств — скорость проверки принадлежности. Проверка, существует ли элемент в множестве, выполняется значительно быстрее, чем в списке, особенно для больших коллекций:
# Проверяем, есть ли пользователь в большой коллекции
active_users_list = []
for i in range(10000):
active_users_list.append("user" + str(i))
# Со списком (медленно для больших коллекций)
print("user5000" in active_users_list) # Checks each element until found
active_users_set = set()
for i in range(10000):
active_users_set.add("user" + str(i))
# С множеством (быстро независимо от размера)
print("user5000" in active_users_set) # Direct lookupХотя оба варианта дают один и тот же результат, версия с множеством работает драматически быстрее на больших коллекциях. Это потому, что множества используют хеш-таблицу(hash table) внутри, что позволяет выполнять почти мгновенный поиск независимо от размера, тогда как спискам приходится проверять каждый элемент последовательно.
17.5.2) Используйте множества для устранения дубликатов
Когда нужно удалить дубликаты из коллекции, преобразование в множество — самый простой подход:
# Удаление дубликатов из пользовательского ввода
survey_responses = [
"yes", "no", "yes", "maybe", "yes", "no", "maybe", "yes"
]
# Получаем уникальные ответы
unique_responses = set(survey_responses)
print(unique_responses) # Output: {'yes', 'no', 'maybe'}
# Если нужна обратно версия в виде списка (без дубликатов)
unique_list = list(unique_responses)
print(unique_list) # Output: ['yes', 'no', 'maybe'] (order may vary)17.5.3) Используйте множества для математических операций над множествами
Когда нужно найти общие элементы, различия или объединения между коллекциями, множества предоставляют ясные и эффективные операции:
# Анализ паттернов покупок клиентов
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# Клиенты, купившие оба продукта
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}
# Клиенты, купившие только продукт A
only_a = customers_product_a - customers_product_b
print(f"Only product A: {only_a}")
# Output: Only product A: {101, 102}
# Все клиенты, купившие хотя бы один продукт
all_customers = customers_product_a | customers_product_b
print(f"Total customers: {len(all_customers)}")
# Output: Total customers: 717.5.4) Используйте списки, когда важен порядок
Множества неупорядочены, поэтому, если последовательность элементов важна, нужно использовать список(list):
# WRONG - Порядок не сохраняется в множествах
task_order = {"wake up", "breakfast", "work", "lunch", "work", "dinner"}
print(task_order) # Order is unpredictable and "work" appears only once
# CORRECT - Используйте список, когда важен порядок
task_order = ["wake up", "breakfast", "work", "lunch", "work", "dinner"]
print(task_order)
# Output: ['wake up', 'breakfast', 'work', 'lunch', 'work', 'dinner']17.5.5) Используйте списки, когда дубликаты имеют смысл
Если повторяющиеся значения несут информацию (например, частоту или множественные появления), используйте список(list):
# Запись результатов викторины (дубликаты показывают, сколько студентов получили каждый балл)
quiz_scores = [85, 90, 85, 78, 90, 92, 85, 88]
# Со списком можно посчитать количество вхождений
score_85_count = quiz_scores.count(85)
print(f"Students who scored 85: {score_85_count}")
# Output: Students who scored 85: 3
# С множеством мы потеряли бы эту информацию
unique_scores = set(quiz_scores)
print(unique_scores) # Output: {78, 85, 88, 90, 92}
# Мы не можем сказать, сколько студентов получили каждый балл17.5.6) Используйте списки, когда нужна индексация
Множества не поддерживают индексацию, потому что они неупорядочены. Если нужно обращаться к элементам по позиции, используйте список(list):
# WRONG - Множества не поддерживают индексацию
colors = {"red", "blue", "green"}
# first_color = colors[0] # Raises: TypeError: 'set' object is not subscriptable
# CORRECT - Используйте список для доступа по индексу
colors = ["red", "blue", "green"]
first_color = colors[0]
print(first_color) # Output: red17.6) Frozenset: неизменяемые множества
До сих пор мы работали с обычными множествами, которые изменяемы — вы можете добавлять и удалять элементы после создания. Python также предоставляет тип frozenset, который представляет собой неизменяемую версию множества. После создания frozenset нельзя модифицировать.
17.6.1) Создание frozenset
Frozenset создаётся с помощью конструктора frozenset(), аналогично тому, как создаётся обычное множество через set():
# Создание frozenset из списка
colors = frozenset(["red", "blue", "green"])
print(colors) # Output: frozenset({'red', 'blue', 'green'})
print(type(colors)) # Output: <class 'frozenset'>
# Создание frozenset из кортежа
numbers = frozenset((1, 2, 3, 4, 5))
print(numbers) # Output: frozenset({1, 2, 3, 4, 5})
# Создание пустого frozenset
empty = frozenset()
print(empty) # Output: frozenset()Как и обычные множества, frozenset автоматически устраняют дубликаты:
# Дубликаты удаляются
values = frozenset([1, 2, 2, 3, 3, 3, 4])
print(values) # Output: frozenset({1, 2, 3, 4})17.6.2) Frozenset неизменяемы
После создания frozenset нельзя изменить. Методы вроде add(), remove(), discard(), pop() и clear() не существуют для frozenset:
# Создание frozenset
languages = frozenset(["Python", "JavaScript", "Java"])
# Попытка модификации приводит к ошибке
# languages.add("C++") # AttributeError: 'frozenset' object has no attribute 'add'
# languages.remove("Java") # AttributeError: 'frozenset' object has no attribute 'remove'Эта неизменяемость — определяющая характеристика frozenset. Если вам нужно «изменить» frozenset, необходимо создать новый:
# Исходный frozenset
original = frozenset([1, 2, 3])
# Создание нового frozenset с дополнительным элементом
modified = frozenset(list(original) + [4])
print(original) # Output: frozenset({1, 2, 3})
print(modified) # Output: frozenset({1, 2, 3, 4})17.6.3) Операции над множествами работают с frozenset
Frozenset поддерживают все те же операции над множествами, что и обычные множества (объединение, пересечение, разность и т. д.):
# Операции над множествами с frozenset
set_a = frozenset([1, 2, 3, 4])
set_b = frozenset([3, 4, 5, 6])
# Union
print(set_a | set_b) # Output: frozenset({1, 2, 3, 4, 5, 6})
# Intersection
print(set_a & set_b) # Output: frozenset({3, 4})
# Difference
print(set_a - set_b) # Output: frozenset({1, 2})
# Symmetric difference
print(set_a ^ set_b) # Output: frozenset({1, 2, 5, 6})Также можно смешивать обычные множества и frozenset в операциях:
regular_set = {1, 2, 3}
frozen_set = frozenset([3, 4, 5])
# Операции между обычным множеством и frozenset
result = regular_set | frozen_set
print(result) # Output: {1, 2, 3, 4, 5}
print(type(result)) # Output: <class 'set'> (result is a regular set)17.6.4) Зачем использовать frozenset?
Основная причина использовать frozenset — их можно использовать как ключи словаря(dictionary) или как элементы других множеств, чего нельзя делать с обычными множествами:
# WRONG - Обычные множества не могут быть ключами словаря
# regular_set = {1, 2, 3}
# my_dict = {regular_set: "value"} # TypeError: unhashable type: 'set'
# CORRECT - Frozenset могут быть ключами словаря
frozen_set = frozenset([1, 2, 3])
my_dict = {frozen_set: "value"}
print(my_dict) # Output: {frozenset({1, 2, 3}): 'value'}
print(my_dict[frozen_set]) # Output: valueПрактический пример использования frozenset в качестве ключей словаря:
# Хранение информации о парах координат
# Каждая координата — это frozenset из значений (x, y)
location_data = {
frozenset([0, 0]): "origin",
frozenset([1, 0]): "east",
frozenset([1, 1]): "northeast"
}
# Поиск локации
point = frozenset([1, 0])
print(location_data[point]) # Output: eastFrozenset также могут быть элементами других множеств:
# WRONG - Обычные множества не могут быть элементами множества
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# CORRECT - Frozenset могут быть элементами множества
set_of_frozensets = {
frozenset([1, 2]),
frozenset([3, 4]),
frozenset([5, 6])
}
print(set_of_frozensets)
# Output: {frozenset({1, 2}), frozenset({3, 4}), frozenset({5, 6})}Практический пример представления групп:
# Представление команд, где каждая команда — frozenset из ID игроков
tournament_teams = {
frozenset([101, 102, 103]), # Team A
frozenset([201, 202, 203]), # Team B
frozenset([301, 302, 303]) # Team C
}
# Проверка, зарегистрирована ли конкретная команда
team_to_check = frozenset([101, 102, 103])
if team_to_check in tournament_teams:
print("Team is registered")
else:
print("Team not found")
# Output: Team is registered17.6.5) Преобразование между set и frozenset
Вы можете легко преобразовывать обычные множества и frozenset друг в друга:
# Преобразование обычного множества в frozenset
regular = {1, 2, 3, 4}
frozen = frozenset(regular)
print(frozen) # Output: frozenset({1, 2, 3, 4})
# Преобразование frozenset в обычное множество
frozen = frozenset([5, 6, 7, 8])
regular = set(frozen)
print(regular) # Output: {5, 6, 7, 8}
# Теперь мы можем модифицировать обычное множество
regular.add(9)
print(regular) # Output: {5, 6, 7, 8, 9}17.7) Хешируемые и нехешируемые типы: что может быть ключами словаря или элементами множества (и краткая заметка о хешировании)
На протяжении этой главы мы видели, что множества могут содержать объекты некоторых типов, но не других. Например, вы можете создать множество целых чисел или строк, но не множество списков. Это ограничение существует потому, что элементы множества (и ключи словаря, как мы узнали в главе 16) должны быть хешируемыми(hashable).
17.7.1) Что означает «хешируемый»?
Хешируемый(hashable) объект — это объект, у которого есть хеш-значение, которое никогда не меняется в течение его жизни. Python вычисляет это хеш-значение с помощью встроенной функции hash():
# У хешируемых типов есть хеш-значение
print(hash(42)) # Output: 42
print(hash("Python")) # Output: (some large integer)
print(hash((1, 2, 3))) # Output: (some large integer)Хеш-значение — это целое число, которое Python использует внутренне, чтобы быстро находить объекты в множествах и словарях. Думайте об этом как об адресе или индексе, который помогает Python эффективно находить вещи.
Ключевое свойство: чтобы объект был хешируемым, его хеш-значение должно оставаться постоянным на протяжении всей жизни. Это означает, что сам объект должен быть неизменяемым — если объект мог бы меняться, его хеш-значение тоже должно было бы меняться, что «сломало» бы множества и словари.
17.7.2) Неизменяемые типы хешируемы
Все встроенные неизменяемые типы Python хешируемы и могут использоваться как элементы множеств или ключи словарей:
# Целые числа хешируемы
numbers = {1, 2, 3, 4, 5}
print(numbers) # Output: {1, 2, 3, 4, 5}
# Строки хешируемы
words = {"apple", "banana", "cherry"}
print(words) # Output: {'apple', 'banana', 'cherry'}
# Кортежи хешируемы (если содержат только хешируемые элементы)
coordinates = {(0, 0), (1, 1), (2, 2)}
print(coordinates) # Output: {(0, 0), (1, 1), (2, 2)}
# Frozenset хешируемы
frozen_sets = {frozenset([1, 2]), frozenset([3, 4])}
print(frozen_sets) # Output: {frozenset({1, 2}), frozenset({3, 4})}
# Булевы значения и None хешируемы
mixed = {True, False, None, 42, "text"}
print(mixed) # Output: {False, True, None, 42, 'text'}17.7.3) Изменяемые типы не хешируемы
Изменяемые типы, такие как списки(list), обычные множества и словари(dictionary), не хешируемы, потому что их содержимое может меняться:
# Lists are NOT hashable
# my_set = {[1, 2, 3]} # TypeError: unhashable type: 'list'
# Regular sets are NOT hashable
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# Dictionaries are NOT hashable
# my_set = {{"key": "value"}} # TypeError: unhashable type: 'dict'Почему изменяемость важна? Представьте, что случилось бы, если бы мы могли добавить список в множество:
# Гипотетический сценарий (на самом деле это не работает)
# my_list = [1, 2, 3]
# my_set = {my_list} # Предположим, что это работает
#
# # Python вычисляет хеш на основе [1, 2, 3]
# # Теперь мы модифицируем список:
# my_list.append(4) # Теперь это [1, 2, 3, 4]
#
# # Хеш-значение стало бы неверным! Множество было бы "повреждено".Вот почему Python запрещает изменяемым объектам находиться в множествах или использоваться в качестве ключей словаря — это сломало бы внутреннюю структуру данных.
Распространённая путаница у новичков: хотя сами множества изменяемы (можно добавлять и удалять элементы), элементы должны быть неизменяемыми. Новички иногда пытаются модифицировать объекты после добавления их в множества, не понимая этого концептуального различия:
# Распространённая путаница: множество изменяемо, но элементы должны быть неизменяемыми
# Множество изменяемо — вы можете менять его содержимое
fruits = {'apple', 'banana'}
fruits.add('orange') # ✓ Works
fruits.remove('apple') # ✓ Works
# Но элементы должны быть неизменяемыми — их нельзя изменить
my_list = [1, 2, 3]
# my_set = {my_list} # ✗ TypeError: unhashable type: 'list'
# Why? If you could modify my_list after adding it, the set's internal
# structure would be corrupted.
# Это работает, потому что кортежи неизменяемы
my_tuple = (1, 2, 3)
my_set = {my_tuple} # ✓ Works - tuples can't be modified17.7.4) Особый случай кортежей
Кортежи(tuple) хешируемы только в том случае, если все их элементы хешируемы. Кортеж, содержащий изменяемые объекты, не хешируем:
# Кортеж только с неизменяемыми элементами — хешируем
good_tuple = (1, 2, "three")
my_set = {good_tuple} # Works: good_tuple is hashable
print(my_set) # Output: {(1, 2, 'three')}
# Кортеж, содержащий список — НЕ хешируем
bad_tuple = (1, 2, [3, 4])
# my_set = {bad_tuple} # TypeError: unhashable type: 'list'Это логично: хотя сам кортеж неизменяем (вы не можете поменять, какие объекты он содержит), если один из этих объектов изменяем, общее «значение» кортежа может измениться:
# Демонстрация, почему кортежи с изменяемыми элементами не могут хешироваться
inner_list = [1, 2]
my_tuple = (inner_list, 3)
# Структура кортежа фиксирована, но список внутри может меняться
inner_list.append(3) # Now inner_list is [1, 2, 3]
# Теперь кортеж «содержит» другие данные, но это всё тот же объект кортежа17.7.5) Проверка хешируемости
Вы можете проверить, является ли объект хешируемым, попытавшись вычислить его хеш:
# Проверка хешируемости
def is_hashable(obj):
"""Проверить, является ли объект хешируемым."""
try:
hash(obj)
return True
except TypeError:
return False
# Тестируем разные типы
print(is_hashable(42)) # Output: True
print(is_hashable("text")) # Output: True
print(is_hashable((1, 2, 3))) # Output: True
print(is_hashable([1, 2, 3])) # Output: False
print(is_hashable({1, 2, 3})) # Output: False
print(is_hashable({"key": "value"})) # Output: False17.7.6) Сводка по хешируемым типам
Хешируемые (могут быть элементами множества или ключами словаря):
- Целые числа:
42 - Числа с плавающей точкой:
3.14 - Строки:
"text" - Кортежи (если все элементы хешируемы):
(1, 2, "three") - Frozenset:
frozenset([1, 2, 3]) - Логические значения:
True,False - None:
None
Не хешируемые (не могут быть элементами множества или ключами словаря):
- Списки(list):
[1, 2, 3] - Обычные множества:
{1, 2, 3} - Словари(dictionary):
{"key": "value"} - Кортежи, содержащие нехешируемые элементы:
(1, [2, 3])
Понимание хешируемости помогает выбирать правильные структуры данных и избегать распространённых ошибок при работе с множествами и словарями. Ключевой принцип прост: если объект может изменяться, он не может быть хеширован; если он не может быть хеширован, он не может быть в множестве или использоваться как ключ словаря.