Модуль 4 · Словари, множества и кортежи

Справочник по Python · модуль 4 · словари, множества и кортежи

Глава 4.9

4.9

frozenset
в Python

Неизменяемый вариант множества. Отношение frozenset к set ровно такое же, как кортежа к списку: те же возможности чтения, никаких изменений — и, как следствие, хешируемость. Только благодаря ей набор может стать ключом словаря или элементом другого множества.

Категория
Структуры данных
Уровень
Средний
Связанные темы
Множества, хеширование, кортежи
Зачем это знать
Использовать набор как ключ и защищать состав от изменений

§1Зачем нужен ещё один тип

Глава 4.5 закончилась ограничением: множество нельзя положить внутрь другого множества и нельзя сделать ключом словаря. Причина известна из главы 4.1 — set изменяем, а значит не хешируем.

main.pyмножество внутри множества
groups = {{1, 2}}
Ошибказнакомая формулировка
TypeError: unhashable type: 'set'

frozenset закрывает именно этот пробел. Он делает для множества то же, что кортеж делает для списка: убирает изменяемость и тем самым возвращает хешируемость.

Схемадве пары «изменяемое и неизменяемое»
list   →  tuple        порядок сохраняется, менять нельзя
set    →  frozenset    уникальность сохраняется, менять нельзя

§2set и frozenset

Свойствоsetfrozenset
уникальные элементыдада
позиционный порядокнетнет
перебор, len(), inдада
операции |, &, -, ^дада
изменение после созданияданет
хешируемостьнетда
ключ словаря, элемент множестванетда
литерал{1, 2}только frozenset(...)

§3Создание

Своего литерала у типа нет — только функция-конструктор, принимающая любую итерируемую коллекцию.

main.pyсписок, строка и пустой набор
permissions = frozenset(["read", "write", "read"])
letters = frozenset("hello")
empty = frozenset()

print(len(permissions))
print(len(letters))
print(empty)
Выводповторы удалены при создании
2
4
frozenset()

Повторы отсеиваются так же, как у set: из двух "read" остался один, а из пяти букв слова hello — четыре.

§4Чтение работает так же

main.pyобычные операции коллекции
roles = frozenset({"reader", "editor"})

print("reader" in roles)
print(len(roles))
print(sorted(roles))
Выводничем не отличается от set
True
2
['editor', 'reader']

Индексов по-прежнему нет: roles[0] даёт TypeError: 'frozenset' object is not subscriptable. Порядок обхода так же не определён, поэтому для воспроизводимого вывода нужен sorted().

§5Методов изменения нет

main.pyпопытка добавить элемент
roles = frozenset({"reader"})
roles.add("admin")
Ошибкатакого метода у типа нет
AttributeError: 'frozenset' object has no attribute 'add'

Здесь AttributeError, а не TypeError: метод не запрещён — его просто не существует. Так же отсутствуют remove(), discard(), pop(), clear() и все методы с суффиксом _update из главы 4.8.

§6Операции создают новый набор

Операции из главы 4.7 доступны полностью — они и раньше не меняли исходные наборы.

main.pyобъединение и разность
base = frozenset({"read", "write"})
extended = base | {"delete"}

print(sorted(extended))
print(sorted(base))
Выводисходный набор не изменился
['delete', 'read', 'write']
['read', 'write']

Недоступны только изменяющие версии: base |= {"delete"} вызовет ошибку, потому что это сокращение от base.update(...).

§7Тип результата

set и frozenset свободно смешиваются в операциях. Тип результата определяет левый операнд.

main.pyодна и та же операция в двух порядках
print(type(frozenset({1}) | {2}))
print(type({1} | frozenset({2})))
Выводтип наследуется от левого операнда
<class 'frozenset'>
<class 'set'>

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

§8Отношения множеств

main.pyпроверки из главы 4.7
required = frozenset({"read", "write"})
granted = frozenset({"read", "write", "delete"})

print(required.issubset(granted))
print(granted.issuperset(required))
print(required.isdisjoint({"ban"}))
Выводвсе проверки работают
True
True
True

Это делает frozenset удобным для наборов прав и ролей: состав зафиксирован типом, а все проверки достаточности остаются доступными.

§9Сравнение с set

Равенство определяется составом, а не типом.

main.pyразные типы, одинаковый состав
mutable = {1, 2}
fixed = frozenset({2, 1})

print(mutable == fixed)
Выводнаборы равны
True

Порядок при создании тоже роли не играет: {1, 2} и {2, 1} — один и тот же набор. Это отличает frozenset от кортежа, где порядок значим, — сравнение будет в §12.

§10Ключ словаря

Главное применение типа. Ключом становится сам набор целиком, и порядок перечисления его не меняет.

main.pyцена тарифа зависит от набора прав
prices = {
    frozenset({"read"}): 100,
    frozenset({"read", "write"}): 250,
}

print(prices[frozenset({"write", "read"})])
Выводключ нашёлся, хотя порядок другой
250

Обратите внимание: искали {"write", "read"}, а в словаре лежит {"read", "write"}. Для frozenset это одно и то же значение с одним и тем же хешем.

§11Элемент множества

Второе применение — множество множеств, невозможное с обычным set.

main.pyтри набора, два уникальных
groups = {
    frozenset({1, 2}),
    frozenset({2, 1}),
    frozenset({3}),
}

print(len(groups))
Выводпервые два набора совпали
2

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

§12frozenset или tuple

Оба типа неизменяемы и хешируемы, поэтому оба годятся в ключи. Выбор определяется тем, важен ли порядок и допустимы ли повторы.

main.pyодни и те же элементы в разном порядке
print(frozenset({1, 2}) == frozenset({2, 1}))
print((1, 2) == (2, 1))
Выводдля набора порядок не важен, для кортежа важен
True
False
НужноТип
порядок значим (координаты, дата)tuple
повторы значимыtuple
набор без порядка (права, теги)frozenset
операции пересечения и разностиfrozenset

Практическое правило: если ключ — это запись из полей, берите кортеж; если набор из элементов, берите frozenset.

§13Практика: кеш по набору

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

main.pyответ находится по любому порядку тегов
cache = {}

key = frozenset({"b", "a"})
cache[key] = "результат"

print(cache[frozenset({"a", "b"})])
Выводсовпадение по составу, а не по записи
результат

С кортежем пришлось бы заранее сортировать элементы, чтобы ("a", "b") и ("b", "a") совпали. frozenset решает это сам.

§14Когда он действительно нужен

В повседневном коде frozenset встречается нечасто. Ситуаций, где он оправдан, всего несколько.

СитуацияПочему frozenset
набор как ключ словаряset не хешируется
множество множествто же ограничение
константа с набором значенийтип запрещает случайную правку
набор передаётся в функциюгарантия, что функция его не изменит

Во всех остальных случаях подходит обычный set. Выбирать frozenset «на всякий случай» не стоит — он лишает возможности дополнять набор.

§15Частые ошибки

Что написаноЧто произойдётКак правильно
roles.add(x) для frozensetAttributeError: метода нетroles = roles | {x}
roles |= {x}ошибка: это изменение на местеroles = roles | {x}
{{1, 2}}TypeError: unhashable type: 'set'{frozenset({1, 2})}
{1} | frozenset({2}) для ключаполучится set — левый операнд решаетfrozenset({2}) | {1}
frozenset({1, [2]})TypeError: элемент не хешируетсявсе элементы должны быть неизменяемыми
roles[0]TypeError: индексов нетперебрать или взять sorted()
ждать литерал вида f{1, 2}такого синтаксиса не существуеттолько frozenset({1, 2})
брать frozenset там, где набор пополняетсякаждое изменение создаёт новый объектобычный set

§16Проверьте себя

Зачем нужен frozenset, если есть set?

Обычное множество изменяемо и потому не хешируется: его нельзя сделать ключом словаря или элементом другого множества. frozenset снимает это ограничение.

Почему roles.add(x) даёт AttributeError, а не TypeError?

Метод не запрещён — его просто не существует у типа, как append() у кортежа.

Какого типа будет результат {1} | frozenset({2})?

set: тип наследуется от левого операнда. Чтобы получить frozenset, он должен стоять слева.

Равны ли {1, 2} и frozenset({2, 1})?

Да. Равенство определяется составом, а не типом и не порядком перечисления.

Когда лучше кортеж, а когда frozenset?

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

Можно ли создать frozenset литералом?

Нет, своего литерала у типа нет — только конструктор frozenset(...).

§17Шпаргалка

Создатьfrozenset({1, 2})
Из спискаfrozenset(items)
Пустойfrozenset()
«Добавить»fs = fs | {x}
Ключ словаряprices[frozenset(perms)]
Множество наборов{frozenset(a), frozenset(b)}
Проверкиfs.issubset(other)

Итог главы

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

Глава 4.9 · frozenset · Справочник по Python Автор: Макаров Максим Николаевич