💻 Примеры кода

⚡ Множество за один экран

nums = {1, 2, 2, 3}   # {1, 2, 3}    — дубликаты пропали сами
empty = set()          # set() — пустые {} создали бы dict, а не set
nums.add(4)             # {1, 2, 3, 4}
nums.discard(100)       # без ошибки, даже если элемента 100 нет

a, b = {1, 2, 3}, {2, 3, 4}
print(a | b, a & b, a - b, a ^ b)
# {1, 2, 3, 4} {2, 3} {1} {1, 4}
Что нужноИнструментПример
Убрать дубликатыset(iterable)set([1, 1, 2]) → {1, 2}
Добавить / убрать элементadd() / discard()s.discard(x) — без ошибки
Общее / разное / всё& | - ^a & b — только общее
«Входит ли в …?»issubset() / issuperset(){1, 2}.issubset(a)
Топ-3 ошибки: {} — это пустой dict, а не set · remove() на отсутствующем элементе роняет KeyError (нужен discard()) · список нельзя положить в множество — он нехешируемый.

Примеры идут от простого к сложному: создание множества → изменение его состава → операции над парой множеств → отношения между множествами → хешируемость и границы применимости. Весь вывод в комментариях получен реальным запуском кода, а не подобран по памяти.

Пример 1. Создание множества и ловушка пустых {}

Множество можно записать литералом в фигурных скобках или получить из любой итерируемой коллекции функцией set() — при этом дубликаты исчезают автоматически.

numbers_set = {1, 2, 3, 2, 1}
print(numbers_set)          # {1, 2, 3}
print(type(numbers_set))    # <class 'set'>

empty_set = set()
empty_dict = {}
print(type(empty_set))      # <class 'set'>
print(type(empty_dict))     # <class 'dict'>

from_list = set([1, 2, 2, 3])
from_string = set("banana")
print(from_list)            # {1, 2, 3}
print(sorted(from_string))  # ['a', 'b', 'n']

Почему так: в Python фигурные скобки исторически заняты словарём, поэтому {} всегда создаёт dict. Единственный способ получить пустое множество — вызвать set(). Эта ловушка встречается настолько часто, что отдельно разобрана в типичных ошибках урока.

Пример 2. Set comprehension

Как и list comprehension из урока 30, множество можно построить выражением в фигурных скобках — с той же логикой цикла и условия, но результат уже без повторов.

squares = {x ** 2 for x in range(-3, 4)}
print(sorted(squares))   # [0, 1, 4, 9]

words = ["python", "java", "python", "sql", "java"]
lengths = {len(w) for w in words}
print(sorted(lengths))   # [3, 4, 6]

Что происходит: отрицательные и положительные аргументы дают одинаковые квадраты ((-3)**2 == 3**2), поэтому в squares они схлопываются в один элемент — из 7 значений range(-3, 4) получилось 4 уникальных квадрата. Так же lengths оставляет только различающиеся длины слов, хотя слов было пять.

Пример 3. Добавление элементов: add() и update()

Множество изменяемо: в него можно дозаписывать элементы после создания. add() добавляет один элемент, update() — сразу несколько из любой итерируемой коллекции.

s = {1, 2, 3}
s.add(4)
print(sorted(s))            # [1, 2, 3, 4]

s.add(2)                    # элемент уже есть
print(sorted(s))            # [1, 2, 3, 4] — без изменений

s.update([5, 6], {7})
print(sorted(s))            # [1, 2, 3, 4, 5, 6, 7]

Что происходит: повторное add(2) ничего не меняет — множество и так гарантирует уникальность, поэтому «добавить уже существующий элемент» не ошибка, а просто действие без эффекта. update() принимает сразу несколько источников и объединяет их все за один вызов.

Пример 4. Удаление: remove(), discard(), pop(), clear()

Четыре способа удаления ведут себя по-разному на отсутствующем элементе и на пустом множестве — от этого различия чаще всего зависит выбор метода.

s2 = {1, 2, 3, 4, 5}
s2.remove(5)
print(sorted(s2))           # [1, 2, 3, 4]

try:
    s2.remove(100)
except KeyError as e:
    print(f"KeyError: {e}")  # KeyError: 100

s2.discard(100)             # тот же отсутствующий элемент, но без ошибки
print(sorted(s2))           # [1, 2, 3, 4]

popped = s2.pop()
print("popped:", popped, "остаток:", sorted(s2))
# popped: 1 остаток: [2, 3, 4]

s2.clear()
print(s2)                   # set()

Разница remove и discard: remove() подходит, когда отсутствие элемента — это ошибка в логике программы и её нужно заметить. discard() — когда «удалить, если есть» является ожидаемым сценарием. pop() удаляет и возвращает первый элемент, который найдёт множество, — конкретное значение зависит от внутреннего хеш-порядка, а не от того, что было добавлено раньше.

Пример 5. Операции над множествами: операторы и одноимённые методы

Объединение, пересечение, разность и симметрическая разность — четыре базовые операции над парой множеств. У каждой есть оператор и метод с тем же результатом.

set1 = {1, 2, 3}
set2 = {2, 3, 4}

print(sorted(set1 | set2), sorted(set1.union(set2)))
# [1, 2, 3, 4] [1, 2, 3, 4]                — объединение: всё из обоих

print(sorted(set1 & set2), sorted(set1.intersection(set2)))
# [2, 3] [2, 3]                            — пересечение: только общее

print(sorted(set1 - set2), sorted(set1.difference(set2)))
# [1] [1]                                  — разность: только в set1

print(sorted(set2 - set1), sorted(set2.difference(set1)))
# [4] [4]                                  — разность не симметрична

print(sorted(set1 ^ set2), sorted(set1.symmetric_difference(set2)))
# [1, 4] [1, 4]                            — то, что не общее

Что происходит: set1 - set2 и set2 - set1 дают разные результаты — разность зависит от порядка операндов, в отличие от объединения и пересечения. Все шесть вызовов вернули новые множества: ни set1, ни set2 при этом не изменились.

Пример 6. In-place варианты: update(), intersection_update(), difference_update()

У операций из примера 5 есть версии с суффиксом _update — они не возвращают новое множество, а изменяют исходное на месте.

a = {1, 2, 3, 4}
b = {3, 4, 5}

a_copy = set(a)
a_copy.intersection_update(b)
print(sorted(a_copy))       # [3, 4]

a_copy2 = set(a)
a_copy2.difference_update(b)
print(sorted(a_copy2))      # [1, 2]

a_copy3 = set(a)
a_copy3.update(b)
print(sorted(a_copy3))      # [1, 2, 3, 4, 5]

print("исходное a не тронуто:", sorted(a))   # [1, 2, 3, 4]
Путаница update и union: a.union(b) возвращает новое множество и не трогает a. a.update(b) ничего не возвращает (None) и изменяет a на месте. Перепутать их — значит либо потерять результат union(), не сохранив его в переменную, либо неожиданно изменить исходное множество вызовом update().

Пример 7. Отношения множеств: issubset(), issuperset(), isdisjoint()

Кроме операций, дающих новое множество, есть проверки, дающие True/False: входит ли одно множество в другое, и есть ли у них общие элементы вообще.

set1 = {1, 2, 3}
set3 = {1, 2}

print(set3.issubset(set1), set3 <= set1)      # True True
print(set1.issuperset(set3), set1 >= set3)    # True True

print({5, 6, 7}.isdisjoint({8, 9, 10}))       # True  — общих элементов нет
print({1}.isdisjoint({1, 2, 3}))              # False — общий элемент 1 есть

print(set1 == {3, 2, 1})                      # True  — состав одинаковый
print(set1 != {2, 3, 4})                      # True

Что происходит: <= и >= — операторные синонимы issubset()/issuperset(), как & для intersection(). Равенство множеств (==) сравнивает только состав элементов, а не порядок — {1, 2, 3} и {3, 2, 1} равны, хотя записаны в разном порядке.

Пример 8. Хешируемость элементов и frozenset

Элементом множества может быть только хешируемый объект — то есть объект с постоянным на всё время жизни хешем. Списки хешируемыми не бывают, а неизменяемый аналог множества — frozenset.

print(hash(42))              # 42
print(hash((1, 2, 3)))       # число, стабильное в рамках запуска

try:
    hash([1, 2, 3])
except TypeError as e:
    print(f"TypeError: {e}")  # TypeError: unhashable type: 'list'

try:
    bad_set = {[1, 2]}
except TypeError as e:
    print(f"TypeError: {e}")  # TypeError: unhashable type: 'list'

frozen = frozenset([1, 2, 3])
print(frozen)                 # frozenset({1, 2, 3})
try:
    frozen.add(4)
except AttributeError as e:
    print(f"AttributeError: {e}")
    # AttributeError: 'frozenset' object has no attribute 'add'

nested = {frozenset([1, 2]), frozenset([3, 4])}   # frozenset хешируем — можно класть в set
print(nested)   # {frozenset({3, 4}), frozenset({1, 2})}

Почему так: список изменяемый, поэтому его хеш не может быть постоянным — Python сразу запрещает и hash([...]), и попытку положить список в множество. frozenset — неизменяемая версия множества: методов изменения (add, remove, …) у него нет, зато сам он хешируем и может быть элементом другого множества или ключом словаря. Развёрнуто про хеш-таблицы и связку frozenset со словарями — в уроке 34.

Хешируемость ≠ неизменяемость буквально: объект хешируем, если у него определён стабильный __hash__ — обычно это совпадает с неизменяемостью (числа, строки, кортежи из хешируемых элементов), но формально Python проверяет именно наличие и стабильность __hash__, а не факт запрета на изменение атрибутов.

Пример 9. Практика: дедупликация и быстрая проверка «есть ли элемент»

Два самых частых применения множества в реальном коде: убрать повторы и проверить вхождение значения намного быстрее, чем перебором списка.

raw = [3, 1, 2, 3, 1, 4, 2]
print(sorted(set(raw)))     # [1, 2, 3, 4]

visited_ids = {101, 205, 310}
print(205 in visited_ids)   # True
print(999 in visited_ids)   # False

text = "hello world"
unique_chars = set(text)
unique_chars.discard(" ")
print(sorted(unique_chars))
# ['d', 'e', 'h', 'l', 'o', 'r', 'w']

Почему так быстро: множество в CPython устроено как хеш-таблица — проверка x in my_set в среднем занимает постоянное время O(1), потому что интерпретатор сразу вычисляет по хешу элемента, куда «идти», а не сравнивает по очереди со всеми элементами, как x in my_list (O(n)). На маленьких коллекциях разница незаметна, но на десятках тысяч элементов — уже существенна.

Пример 10. Границы применимости: порядок не гарантирован, только хешируемое

Последний пример фиксирует два правила, которые нарушают чаще всего: на порядок элементов множества нельзя полагаться, а класть в него можно только хешируемые значения.

demo = {"banana", "apple", "cherry"}
print(demo)          # порядок вывода не является частью контракта set
print(sorted(demo))  # ['apple', 'banana', 'cherry'] — стабильно, если порядок важен

list1 = [1, 2, 3, 4, 5]
list2 = [3, 4, 5, 6, 7]
common = sorted(set(list1) & set(list2))
print(common)         # [3, 4, 5]

Что делать на практике: если результат нужно вывести пользователю, сравнить в тесте или записать в файл — оборачивайте в sorted(). Если порядок неважен (например, множество используется только для проверки in), оборачивать не нужно. Кортеж, в отличие от списка, хешируем и потому годится элементом множества — это разобрано в уроке 24; список из урока 18 — нет.

Что делать дальше

Разберите файлы репозитория — там те же идеи в исходном виде, затем переходите к заданиям. Если что-то из примеров не запустилось — сначала загляните в типичные ошибки.