Множества (set) в Python: полное руководство по всем методам
Изучите множества Python — создание, уникальность, алгебра множеств и все встроенные методы (union, intersection, difference, symmetric_difference и другие) с рабочими примерами и разбором того, что мутирует, а что возвращает новый объект.
set в Python — это неупорядоченная коллекция уникальных хешируемых элементов. Это тот инструмент, к которому стоит обращаться всякий раз, когда нужны быстрые проверки принадлежности, нужно избавиться от дубликатов в последовательности или требуется выполнить математические операции над множествами вроде объединения и пересечения прямо в коде. Это руководство охватывает всё: от способов создания множеств до полного справочника по методам с рабочими примерами.
Основы множеств
Уникальность и хешируемость
Множество никогда не может содержать дубликаты элементов — добавление значения, которое уже присутствует, просто ничего не делает. Кроме того, каждый элемент множества должен быть хешируемым: неизменяемые типы, такие как int, str, float и tuple (из хешируемых элементов), подходят без проблем, а вот изменяемые типы вроде list, dict и других объектов set хранить внутри множества нельзя.
1
2
3
4
s = {1, 2, 2, 3}
print(s) # {1, 2, 3}
# s.add([1, 2]) # TypeError: unhashable type: 'list'
Создание множеств
Есть три распространённых способа создать множество:
1
2
3
4
5
literal = {1, 2, 3}
from_constructor = set([1, 2, 2, 3]) # {1, 2, 3}
from_comprehension = {x * x for x in range(5)} # {0, 1, 4, 9, 16}
empty = set() # НЕ {} — это создаёт пустой словарь
frozenset — неизменяемый собрат set
frozenset ведёт себя как set, но не может быть изменён после создания, что делает его хешируемым и пригодным для использования в качестве ключа словаря или элемента внутри другого множества.
1
2
fs = frozenset([1, 2, 3])
d = {fs: "immutable key works"}
Принадлежность и итерация
Проверка принадлежности элемента множеству выполняется в среднем за O(1), тогда как для списка это O(n) — именно поэтому множество стоит выбирать, когда нужно многократно проверять «есть ли это значение».
1
2
3
4
5
colors = {"red", "green", "blue"}
print("red" in colors) # True
for c in colors: # порядок не гарантирован
print(c)
Множества неупорядочены
Поскольку множества построены на хеш-таблицах, они не сохраняют порядок добавления элементов и не поддерживают индексацию (colors[0] вызовет TypeError). Если порядок важен, используйте list либо, начиная с Python 3.7, полагайтесь на dict (который сохраняет порядок вставки).
Методы множеств
Ниже перечислены все основные методы множеств: с сигнатурой, коротким рабочим примером и — что важнее всего — указанием, возвращает ли метод новое множество или изменяет исходное на месте.
set.add()
Добавляет один элемент в множество. Изменяет множество на месте; возвращает None.
1
2
3
s = {1, 2}
s.add(3)
print(s) # {1, 2, 3}
set.clear()
Удаляет все элементы множества. Изменяет множество на месте; возвращает None.
1
2
3
s = {1, 2, 3}
s.clear()
print(s) # set()
set.copy()
Возвращает поверхностную копию множества — по-настоящему новый объект, поэтому изменение копии не затрагивает оригинал.
1
2
3
4
s = {1, 2, 3}
c = s.copy()
c.add(4)
print(s, c) # {1, 2, 3} {1, 2, 3, 4}
set.difference()
set.difference(*others) — возвращает новое множество из элементов set, которых нет ни в одном из others. Эквивалент оператора -.
1
2
3
4
a = {1, 2, 3}
b = {2, 3, 4}
print(a.difference(b)) # {1}
print(a - b) # {1}
set.difference_update()
Выполняет те же вычисления, что и difference(), но изменяет set на месте вместо того, чтобы возвращать новое множество.
1
2
3
4
a = {1, 2, 3}
b = {2, 3, 4}
a.difference_update(b)
print(a) # {1}
set.discard()
Удаляет элемент, если он присутствует; в отличие от remove(), не вызывает исключение, если элемента нет. Изменяет множество на месте; возвращает None.
1
2
3
4
s = {1, 2, 3}
s.discard(5) # без ошибки
s.discard(2)
print(s) # {1, 3}
set.intersection()
set.intersection(*others) — возвращает новое множество из элементов, общих для set и всех others. Эквивалент оператора &.
1
2
3
4
a = {1, 2, 3}
b = {2, 3, 4}
print(a.intersection(b)) # {2, 3}
print(a & b) # {2, 3}
set.intersection_update()
Выполняет те же вычисления, что и intersection(), но изменяет set на месте.
1
2
3
4
a = {1, 2, 3}
b = {2, 3, 4}
a.intersection_update(b)
print(a) # {2, 3}
set.isdisjoint()
Возвращает True, если set и other не имеют общих элементов. Только чтение — никогда не изменяет множество.
1
2
3
a = {1, 2}
b = {3, 4}
print(a.isdisjoint(b)) # True
set.issubset()
Возвращает True, если каждый элемент set также содержится в other. Эквивалент оператора <=. Только чтение.
1
2
3
4
a = {1, 2}
b = {1, 2, 3}
print(a.issubset(b)) # True
print(a <= b) # True
set.issuperset()
Возвращает True, если set содержит каждый элемент other. Эквивалент оператора >=. Только чтение.
1
2
3
4
a = {1, 2, 3}
b = {1, 2}
print(a.issuperset(b)) # True
print(a >= b) # True
set.pop()
Удаляет и возвращает произвольный элемент. Изменяет множество на месте; вызывает KeyError, если множество пустое.
1
2
3
s = {1, 2, 3}
x = s.pop()
print(x, s) # например, 1 {2, 3} — какой именно элемент будет извлечён, не гарантируется
set.remove()
Удаляет конкретный элемент. Изменяет множество на месте; вызывает KeyError, если элемента нет (используйте discard(), если нужно, чтобы отсутствие элемента просто игнорировалось).
1
2
3
s = {1, 2, 3}
s.remove(2)
print(s) # {1, 3}
set.symmetric_difference()
Возвращает новое множество из элементов, входящих ровно в одно из двух множеств (присутствуют в одном, но не в обоих). Эквивалент оператора ^.
1
2
3
4
a = {1, 2, 3}
b = {2, 3, 4}
print(a.symmetric_difference(b)) # {1, 4}
print(a ^ b) # {1, 4}
set.symmetric_difference_update()
Выполняет те же вычисления, что и symmetric_difference(), но изменяет set на месте.
1
2
3
4
a = {1, 2, 3}
b = {2, 3, 4}
a.symmetric_difference_update(b)
print(a) # {1, 4}
set.union()
set.union(*others) — возвращает новое множество, содержащее все элементы set и всех others. Эквивалент оператора |.
1
2
3
4
a = {1, 2}
b = {2, 3}
print(a.union(b)) # {1, 2, 3}
print(a | b) # {1, 2, 3}
set.update()
Выполняет те же вычисления, что и union(), но изменяет set на месте, добавляя элементы из других итерируемых объектов.
1
2
3
4
a = {1, 2}
b = {2, 3}
a.update(b)
print(a) # {1, 2, 3}
Шпаргалка: методы против операторов
| Метод | Эквивалентный оператор | Возвращает новое или изменяет |
|---|---|---|
union() | \| | Возвращает новое |
update() | \|= | Изменяет на месте |
intersection() | & | Возвращает новое |
intersection_update() | &= | Изменяет на месте |
difference() | - | Возвращает новое |
difference_update() | -= | Изменяет на месте |
symmetric_difference() | ^ | Возвращает новое |
symmetric_difference_update() | ^= | Изменяет на месте |
issubset() | <= | Возвращает bool |
issuperset() | >= | Возвращает bool |
add() | — | Изменяет на месте |
remove() | — | Изменяет на месте (вызывает исключение при отсутствии) |
discard() | — | Изменяет на месте (молча, если элемента нет) |
pop() | — | Изменяет на месте, возвращает удалённый элемент |
clear() | — | Изменяет на месте |
copy() | — | Возвращает новое |
isdisjoint() | — | Возвращает bool |
Правило, которое стоит запомнить: операторные формы (|, &, -, ^) и одноимённые методы всегда возвращают новое множество, а формы с суффиксом _update (или операторы вида =) изменяют исходное множество на месте — это полностью повторяет разницу между + и += для списков.
Типичные сценарии использования
Удаление дубликатов из списка. Самый быстрый способ убрать дубликаты, пожертвовав порядком, — это list(set(my_list)). Если порядок важен, используйте вместо этого dict.fromkeys(my_list), поскольку множества не гарантируют сохранение порядка.
1
2
raw = [3, 1, 2, 3, 1, 4]
unique = list(set(raw)) # порядок не гарантирован
Быстрая проверка принадлежности. Преобразование большого списка в множество перед множеством проверок in превращает поиск с O(n) в поиск с O(1), что особенно важно в циклах.
1
2
3
4
allowed = set(load_allowed_ids()) # один раз
for record in records:
if record.id in allowed: # O(1) на каждой итерации
process(record)
Поиск пересечений и различий между наборами данных. Алгебра множеств — естественное решение всякий раз, когда нужно сравнить две коллекции: например, какие теги встречаются в обеих из двух статей или какие пользователи отписались с прошлого месяца.
1
2
3
4
tags_a = {"python", "ai", "tutorial"}
tags_b = {"python", "ml"}
shared = tags_a & tags_b # {'python'}
only_in_a = tags_a - tags_b # {'ai', 'tutorial'}
Заметки о производительности
В CPython множества реализованы как хеш-таблицы — та же базовая структура, что и у словарей, только без значений. Благодаря этому операции add, remove, discard и in в среднем выполняются за O(1), тогда как эквивалентные операции над списком требуют O(n). Плата за это — расход памяти: множество, как правило, потребляет больше памяти на элемент, чем список с теми же данными, — а также отсутствие порядка и индексации. Для небольших коллекций (несколько элементов) разница незаметна; но там, где данные обрабатываются в «горячем» цикле или проверяются многократно, множества почти всегда предпочтительнее списков.
Типичные ошибки
- Литерал пустого множества.
{}создаётdict, а неset. Для пустого множества всегда используйтеset(). - Нехешируемые элементы. Попытка поместить
listилиdictв множество вызоветTypeError: unhashable type. Если вложенные списки всё же нужно хранить, сначала преобразуйте их в кортежи. - Расчёт на порядок. Поскольку множества неупорядочены, не стоит полагаться ни на порядок итерации, ни на предсказуемость значения, возвращаемого
pop(). Если важна детерминированность, отсортируйте множество или используйте список. - Путаница между
remove()иdiscard(). Используйтеdiscard(), когда элемента может не быть и вы не хотите получать исключение; используйтеremove(), когда отсутствие элемента сигнализирует об ошибке, которую нужно заметить.
Часто задаваемые вопросы
Какой метод удаляет и возвращает произвольный элемент множества? pop(). Он не принимает аргументов — нет ни «извлечения по имени», ни извлечения по индексу, потому что у множества нет ни позиций, ни ключей. Чтобы удалить конкретный элемент, используйте remove() (вызывает KeyError, если элемента нет) или discard() (молча ничего не делает, если элемента нет).
Является ли set.pop() случайным? Нет — «произвольный» не значит «случайный». CPython возвращает первый элемент из внутренней хеш-таблицы множества: для конкретного множества это детерминированное поведение, но оно зависит от хеш-значений и истории вставок, поэтому полагаться на то, какой именно элемент вы получите, нельзя. Если нужен по-настоящему случайный выбор, используйте random.choice(tuple(s)).
Что делает add(), если элемент уже существует? Ничего, и исключение при этом не возникает. Множество хеширует значение, находит слот, уже занятый равным элементом, и возвращает None, не меняя ничего — тот же None, что возвращается и при успешной вставке. Единственный надёжный способ понять, действительно ли add() что-то добавил, — сравнить len(s) до и после вызова.
Для чего нужен discard() и удаляет ли он дубликаты? discard(x) удаляет x, если он присутствует, и ничего не делает, если его нет. Удалить «все копии» элемента он не может в принципе, потому что множество изначально не хранит дубликатов — в множестве может быть не более одного экземпляра любого значения.
Что произойдёт, если попытаться добавить элемент во frozenset или удалить его оттуда? Возникнет AttributeError. frozenset неизменяем, поэтому у него попросту нет методов add, remove, discard, pop, clear и *_update. Он не преобразуется в обычное множество и не очищается — вызов просто завершится ошибкой. Вместо этого нужно создать новый frozenset: fs | {new_item}.
Множества называют неизменяемыми, но как тогда можно добавлять и удалять элементы? Здесь речь о двух разных вещах. Само множество изменяемо — элементы можно свободно добавлять и удалять. А вот элементы — нет: они обязаны быть хешируемыми, и значение хешируемого объекта не должно меняться, пока он находится в множестве. Именно поэтому список никогда не может быть элементом множества, а кортеж из неизменяемых значений — может.
Возвращает ли difference_update() новое множество? Нет, он возвращает None — как и любой метод с суффиксом _update (intersection_update, symmetric_difference_update, update), он изменяет множество на месте. После result = s.difference_update(t) переменная result будет равна None; если нужно получить значение обратно, используйте s.difference(t).
Как очистить множество? s.clear() очищает его на месте, поэтому любая другая ссылка на тот же объект множества тоже увидит его пустым. А вот повторное связывание через s = set() создаёт новое пустое множество и оставляет любые другие ссылки указывающими на старое, всё ещё заполненное множество.
Как множества вписываются в остальную экосистему Python
Если вы сравниваете множества с другими базовыми контейнерами Python, полное руководство по спискам Python рассказывает об упорядоченных коллекциях с индексацией, а многие операции над множествами (например, sorted(some_set)) опираются на справочник встроенных функций Python — для таких вещей, как len(), sorted() и map(). Множества также естественно подходят для удаления дубликатов токенов или элементов словаря при построении пайплайнов поиска — см. руководство для начинающих по LangChain в Python с практическим примером, где важны быстрые проверки принадлежности.
