Множества и словари Python: hash и общие механизмы
Множество и словарь построены на одном механизме: hash назначает каждому элементу полку, и проверка in становится мгновенной. Ключи словаря — готовое множество, над которым работают & и |.
Редакция Питоники
Пятнадцать уроков разделяли две структуры: множество хранит уникальные элементы из урока 1, словарь — пары «ключ: значение» из курса про словари. Но под обеими лежит один и тот же механизм, и это не совпадение: множество — почти словарь без значений. Понимание общего механизма отвечает сразу на три вопроса, которые накопились за курс: почему проверка in мгновенна, почему dict.fromkeys из урока 16 сохраняет порядок, а set нет, и почему список нельзя положить ни в множество, ни в ключ словаря.
hash: общий фундамент
Хеш — это число, которое Python вычисляет из значения. Не случайное число: одно и то же значение внутри одного запуска программы даёт один и тот же хеш. Словарь и множество используют его как номер полки: положить пару или элемент на полку, найти — сразу по номеру, не перебирая всё содержимое:
print(hash('Анна') == hash('Анна'))
print(hash((1, 2)) == hash((1, 2)))
print(hash((1, 2)) == hash((2, 1)))
print(type(hash('Анна')).__name__)
True True False int
Последняя строка напоминает, что хеш — обычное целое число: type его имени возвращает int. Главное в выводе другое. Равные значения дают равные хеши — иначе ни словарь, ни множество не смогли бы находить элементы. А перестановка в кортеже изменила хеш: хеш чувствителен к составу и порядку, и потому кортежи (1, 2) и (2, 1) — разные ключи.
Ключи словаря — это множество
Ключи словаря и элементы множества подчиняются одним правилам: уникальность, мгновенный in, отсутствие порядка у структуры хранения. Неудивительно, что ключи превращаются в множество одним вызовом:
prices = {'хлеб': 40, 'молоко': 90, 'сыр': 700}
print(len(prices.keys()))
print(sorted(set(prices)))
print('сыр' in prices)
print(set(prices) == {'хлеб', 'молоко', 'сыр'})
3 ['молоко', 'сыр', 'хлеб'] True True
set(prices) собрал множество из ключей — значений в нём не оказалось. Проверка in для словаря ищет среди ключей — тем же механизмом, что и in для множества. Печать через sorted — наше старое правило: множество печатаем только отсортированным.
Операции множеств над ключами двух словарей
Как только ключи стали множествами, вся арифметика из уроков про объединение и пересечение работает над словарями напрямую. Классика прайс-мониторинга: какие товары есть в обоих магазинах, какие только в одном, где цена изменилась:
mon = {'хлеб': 40, 'молоко': 90, 'сыр': 700}
tue = {'молоко': 95, 'сыр': 650, 'сок': 120}
common = set(mon) & set(tue)
only_mon = set(mon) - set(tue)
changed = []
for name in common:
if mon[name] != tue[name]:
changed.append(name)
print('в обоих магазинах:', sorted(common))
print('только в первом:', sorted(only_mon))
print('цена изменилась:', sorted(changed))
в обоих магазинах: ['молоко', 'сыр'] только в первом: ['хлеб'] цена изменилась: ['молоко', 'сыр']
Пересечение нашло общие товары, разность — уникальные первого. А изменившиеся цены — это пересечение, прогнанное через сравнение значений: сами множества о ценах не знают, за значениями возвращаемся в словарь по ключу. Схема «множество отвечает за состав, словарь — за значения» повторяется в половине задач на сравнение данных.
views: операции множеств без set()
Оборачивать словарь в set не обязательно: представление keys() само поддерживает операции множеств — и это самый короткий синтаксис сравнения двух словарей:
mon = {'хлеб': 40, 'молоко': 90}
tue = {'молоко': 95, 'сыр': 650}
print(sorted(mon.keys() & tue.keys()))
print(sorted(mon.keys() | tue.keys()))
print(sorted(mon.keys() - tue.keys()))
['молоко'] ['молоко', 'сыр', 'хлеб'] ['хлеб']
Три строки сравнили два словаря: общие ключи, все ключи, ключи только первого. Никаких промежуточных множеств создавать не пришлось — представление словаря играет роль множества само. То же работает для values() и items(), но с оговоркой: values() — мультимножество, повторяющиеся значения в нём живут по отдельности.
Экзамен на хешируемость общий
Элемент множества и ключ словаря сдают один и тот же экзамен: хеш обязан существовать и не должен меняться после вставки. Отказ у обоих одинаковый и честно называет причину:
data = [1, 2]
try:
s = {data}
except TypeError as e:
print('в множество:', e)
try:
d = {data: 'пара'}
except TypeError as e:
print('в ключ словаря:', e)
в множество: unhashable type: 'list' в ключ словаря: unhashable type: 'list'
Одна причина — один текст ошибки в обе двери: у списка нет хеша, потому что его содержимое меняется, и полка, выбранная по старому хешу, стала бы врать. Кандидаты на роль элемента множества и ключа словаря — одни и те же: строки, числа, кортежи из неизменяемых значений и frozenset.
frozenset и кортежи в роли ключей
Неизменяемые сборки из уроков 10 и 11 проходят экзамен и открывают составные ключи. frozenset хешируется независимо от порядка вставки элементов, кортеж — с учётом порядка:
roads = {
frozenset({'Анна', 'Борис'}): 12,
frozenset({'Борис', 'Вера'}): 7,
}
point_prices = {(0, 0): 100, (1, 5): 250}
print(roads[frozenset({'Борис', 'Анна'})])
print(point_prices[(1, 5)])
12 250
Дорога между Анной и Борисом нашлась, даже когда города назвали в обратном порядке: frozenset не различает порядок элементов. Цены по координатам, наоборот, зависят от порядка: (1, 5) и (5, 1) были бы двумя разными ключами. Выбор между ними — вопрос о том, важен ли порядок частей в вашем ключе.
d = {}
d[1] = 'целое'
d[1.0] = 'дробное'
d[True] = 'истина'
print(len(d))
print(d[1])
1 истина
Три присваивания, одна пара, и значение — от последней записи: True затерло и целое, и дробное. Словарь не предупреждает: для него это один и тот же ключ, просто названный тремя способами. Если ключи приходят из разных источников — из JSON строками, из счётчика числами — приведите их к одному типу сразу.
a = {'x': 1, 'y': 2}
b = {'y': 2, 'z': 3}
try:
a.values() & b.values()
except TypeError:
print('values() операций множеств не поддерживает')
print(sorted(a.items() & b.items()))
values() операций множеств не поддерживает
[('y', 2)]Что дальше
Общий механизм объяснил три загадки курса: мгновенный in из урока 5 — поиск по полке, а не перебор; ограничение на элементы множеств — тот же экзамен, что у ключей; хешируемость namedtuple из урока 15 — он кортеж. Дальше практика: задачи на множества — уникальные гости и общие друзья, задачи на кортежи — координаты и записи.
Множество — словарь без значений, словарь — множество с значениями. Устроив одни механизм в голове, вы перестаёте запоминать два набора правил и начинаете предсказывать поведение обеих структур по одному общему принципу.
Сначала предскажи ответ в голове — это главный навык программиста.
a = {'x': 1, 'y': 2, 'z': 3}
b = {'y': 20, 'z': 30, 'w': 40}
print(sorted(a.keys() & b.keys()))
print(len(a.keys() | b.keys()))
d = {}
d[(1, 2)] = 'точка'
d[(2, 1)] = 'вторая'
print(d[(1, 2)])
print(len(d))
s = {1, 2}
s.add(True)
print(len(s))
print(sorted(s))
1. Что делает hash() в механизме словаря и множества?
2. Что вернёт set({'хлеб': 40, 'молоко': 90})?
3. Как найти общие ключи двух словарей одной строкой?
4. Почему hash('Анна') в двух запусках программы может различаться?
5. Что общего у экзамена на элемент множества и на ключ словаря?
6. d[1] = 'целое'; d[True] = 'истина' — сколько пар в словаре?
7. Чем frozenset удобен в роли ключа?
Даны два словаря товаров: stock1 = {'чай': 10, 'кофе': 5} и stock2 = {'кофе': 8, 'сахар': 3}. Напечатайте отсортированный список товаров, которые есть в обоих складах, и строку «всего разных товаров: N».
Что общего у множеств и словарей в Python?
Механизм хранения: и элементы множества, и ключи словаря находят свою полку по хешу — числу, вычисленному из значения. Поэтому у обеих структур мгновенная проверка in, уникальные элементы и одинаковый экзамен на хешируемость. Множество — почти словарь без значений.
Урок 5: in — мгновенная проверка принадлежностиКак сравнить ключи двух словарей?
Представления keys() поддерживают операции множеств: d1.keys() & d2.keys() — общие ключи, | — все, - — ключи только первого словаря. Печатайте результат через sorted(): множества и представления не гарантируют порядок.
Почему hash строки меняется между запусками программы?
Python подмешивает в строковые хеши случайное зерно на каждый запуск — защита от атак, подбирающих коллизии. Внутри одного запуска hash('Анна') постоянен, поэтому словарь работает; между запусками и между программами хеш передавать нельзя.
Почему список нельзя положить в множество или ключ словаря?
У списка нет постоянного хеша: содержимое меняется, и полка, выбранная по старому хешу, стала бы врать. Python отвечает TypeError: unhashable type: 'list'. Замена — кортеж tuple(x) или frozenset: неизменяемые сборки с честным хешем.
Урок 10: frozenset — неизменяемое множество1, 1.0 и True — это разные ключи словаря?
Нет: полку выбирает ==, а все три равны. В множестве они сливаются в один элемент, в словаре занимают один ключ — значение перезаписывается последней записью. Проверка len({1, 1.0, True}) даёт 1.
Понравился урок? Сошлитесь на него
«Элемент множества и ключ словаря сдают один и тот же экзамен: хеш обязан существовать и не должен меняться после вставки.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Множества · Урок 10
frozenset: неизменяемое множество в Python
Множество, которое нельзя менять: frozenset создаётся один раз, поддерживает все проверки и операции чтения и годится туда, куда set не пускают — ключом в словарь и элементом другого множества.
Множества · Урок 11
Tuple в Python: неизменяемый кортеж и доступ по индексу
Кортеж хранит фиксированный набор значений и защищает его от изменений: создали — и состав застыл. Создание, доступ по индексу, IndexError и то, почему запятая главнее скобок.
Множества · Урок 16
Как удалить дубликаты из списка в Python одной строкой
Убрать повторы из списка — две строки: set(x) выкидывает дубликаты, dict.fromkeys(x) сохраняет порядок первого вхождения. Разбираем оба приёма и случай, когда множеству нужен ваш ключ.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Словари · Урок 18
Ключи: что можно и что нельзя
Ключом бывают строки, числа и кортежи, но не список: TypeError: unhashable type объясняется устройством словаря, а frozenset спасает, когда нужен набор.
ключи словаря pythonunhashable type list
ООП · Урок 12
Сравнение объектов: __eq__ и __hash__
По умолчанию == для своих классов работает как is: два одинаковых счёта оказываются «не равны». Определяем __eq__ по полям, добавляем __hash__ — и объекты работают в словарях и множествах.
__hash__ pythonобъект как ключ словаря python
Множества · Урок 12
Кортеж против списка в Python: что и когда использовать
У обоих есть индексы, len и in, но список можно менять, а кортеж — нет. Четыре отличия: изменяемость, ключ словаря, память и правило выбора за три секунды.
кортеж ключ словаря pythonразница кортеж список