2.10. Множества

Множество — это неупорядоченная коллекция уникальных элементов. Добавление значения, которое уже присутствует, не имеет эффекта; итерация выдаёт каждое значение ровно один раз. Множества — правильный инструмент, когда важны принадлежность и устранение дубликатов, а порядок — нет.

2.10.1. Создание множества

Используйте фигурные скобки для непустого множества или set() для пустого:

colours = {"red", "green", "blue"}
empty = set()

Фигурные скобки выглядят как литерал dict; {} сам по себе — это пустой словарь, а не пустое множество — одна из исторических случайностей Python. Используйте set() для пустого случая.

set() также строит множество из любого итерируемого объекта, что является стандартным способом удалить дубликаты из последовательности:

nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)

Вывод:

{1, 2, 3, 4}

Порядок вывода может различаться — множества не обещают итерировать в каком-либо определённом порядке.

2.10.2. Множество против словаря

И множества, и словари хранят уникальные элементы в хеш-таблице. Различие — в том, что несёт с собой каждый элемент:

  • dict хранит пары ключ-значение. Поиск ключа возвращает его значение.

  • set хранит только элементы. Поиск элемента сообщает вам, присутствует ли он.

Выбор между этими двумя зависит от того, означает ли что-нибудь значение рядом с каждым элементом:

  • Выбирайте множество, когда рядом с каждым элементом не должно быть никакого значения — вас интересует только, присутствует ли элемент, или вы объединяете группы уникальных элементов с помощью объединения / пересечения.

  • Выбирайте словарь, когда каждый элемент сопоставлен с данными, которые поиск должен извлекать — отображение конфигурации, кеш, счётчик с ключом по имени.

Эти два типа разделяют большую часть внешнего синтаксиса, откуда и происходит большая часть путаницы. Различия в одном блоке:

set

dict

содержит

уникальные элементы

уникальные ключи, каждый со значением

заполненный литерал

{1, 2, 3}

{"a": 1, "b": 2}

пустой литерал

set()

{}

проверка принадлежности

x in s

k in d (только ключи)

получить значение

н/д

d[k]

добавить элемент

s.add(x)

d[k] = v

итерация

выдаёт элементы

выдаёт ключи (используйте d.items() для пар)

Асимметрия между заполненными и пустыми литералами — это подводный камень, о котором стоит сказать:

  • Фигурные скобки с элементами внутри{1, 2, 3} — это литерал множества; фигурные скобки с парами ключ-значение{"a": 1} — это литерал словаря. Парсер различает их по тому, что находится внутри.

  • Фигурные скобки с пустым содержимым{} — это пустой словарь, а не пустое множество. Словари появились первыми; пустой литерал принадлежит им. У пустого множества вообще нет литерала со скобками, и оно должно записываться как set().

Распространённый приём, когда у словаря когда-либо читаются только ключи, — переключиться на множество: это делает намерение очевидным и убирает неиспользуемые значения из памяти.

2.10.3. Добавление и удаление

  • set.add() — вставить один элемент.

  • set.discard() — удалить элемент, если он присутствует, ничего не делать, если его нет.

  • set.remove() — удалить элемент; вызвать KeyError, если он отсутствует.

  • set.clear() — опустошить множество.

s = {1, 2, 3}
s.add(4)
s.discard(99)            # silent: 99 not in s
s.remove(2)
print(s)

Вывод:

{1, 3, 4}

2.10.4. Принадлежность

Оператор in проверяет принадлежность. Для множества это занимает примерно постоянное время независимо от размера — что является основной причиной выбрать множество вместо list, когда вам нужно только спросить «есть ли это значение там»:

if "red" in colours:
    print("colour is allowed")

list с тем же содержимым каждый раз сканировался бы с начала, что приемлемо для десяти элементов, но медленно для десяти тысяч.

2.10.5. Операции над множествами

Два множества можно объединять с помощью обычных математических операций. Каждая имеет как форму оператора, так и форму метода:

  • a | b или a.union(b) — всё, что есть в любом из множеств.

  • a & b или a.intersection(b) — только то, что встречается в обоих.

  • a - b или a.difference(b) — то, что есть в a, но не в b.

  • a ^ b или a.symmetric_difference(b) — то, что есть в одном, но не в обоих.

a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)

Вывод:

{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}

Формы операторов доступны только для чтения; формы методов принимают любой итерируемый объект справа, а не только другое множество (a.union([5, 6])). Выбирайте ту, которая лучше читается в контексте.

2.10.6. Что может входить в множество

Элементы множества должны быть хешируемыми — то же ограничение, что и для ключей dict. int, float, str, bool, bytes и tuple (когда его содержимое само по себе хешируемо) — все работают. list и dict — нет; попытка добавить один из них вызывает TypeError.

2.10.7. frozenset

Обычное set является изменяемым: каждый вызов add / remove / discard меняет объект на месте. Эта изменяемость лишает его права быть хешируемым, поэтому множество не может использоваться как ключ dict или как член другого множества.

frozenset — это неизменяемый аналог. У него те же поиски и операторы (in, |, &, -, ^), что и у set, но нет add / remove и нет методов, которые изменяют его. Поскольку его содержимое никогда не может измениться, хеш frozenset чётко определён — поэтому он является хешируемым:

primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})

palettes = {
    primary: "RGB",
    secondary: "mixed",
}

print(palettes[primary])

Вывод:

RGB

Создавайте frozenset из любого итерируемого объекта — frozenset() для пустого случая, frozenset(some_set), чтобы сделать неизменяемый снимок существующего множества:

snapshot = frozenset(s)         # immutable copy of s
s.add("new")                    # snapshot does not change

Две распространённые причины обратиться к нему:

  • Использование как ключа словаря или члена множества. Везде, где одно значение не может охватить то, что вам нужно, может подойти frozenset значений — «набор признаков, поддерживаемых этим драйвером», «набор выводов, которые использует этот профиль».

  • Фиксация константы. frozenset допустимых имён на уровне модуля не может быть случайно изменён вызывающим кодом; обычное set может. Предпочитайте frozenset для всего, что должно быть доступно только для чтения после создания.