Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 18 из 20 Начальный 20 мин 120 XP

Ключи: что можно и что нельзя

Ключом бывают строки, числа и кортежи, но не список: TypeError: unhashable type объясняется устройством словаря, а frozenset спасает, когда нужен набор.

Редакция Питоники

Восемнадцать уроков подряд ключами были строки: 'Анна', 'молоко', 'tea'. Один раз мелькнули числа в календаре. Пора спросить прямо: а что ещё можно? Вопрос не формальный — от ответа зависит архитектура: ключом-кортежем делают составные адреса «строка, колонка», а попытка поставить список в ключ — одна из самых частых ошибок новичка, с непонятным текстом про unhashable. Разберёмся, что допустимо, что запрещено и почему словарь устроен так строго.

Что работает: строки, числа, кортежи

три вида ключей в одном словаре
d = {'год': 2024, 7: 'семь', (10, 20): 'точка'}

print(d['год'])
print(d[7])
print(d[(10, 20)])
print(hash((10, 20)) == hash((10, 20)))
Вывод
2024
семь
точка
True

Строки, числа и кортежи — три рабочих варианта, и все они неизменяемы: строку нельзя переписать по символу, число не увеличишь, у кортежа нет append. Обратите внимание на запись: числовой ключ и кортеж в квадратных скобках пишутся без кавычек — d[7], d[(10, 20)], кавычки нужны только строкам. Последняя строка вывода — подсказка об устройстве словаря: два одинаковых кортежа дали одинаковый хеш, и потому d[(10, 20)] нашёл пару, хотя кортеж для записи и кортеж для доступа — два разных объекта.

Список ключом быть не может

попытка поставить список в ключ
d = {[1, 2]: 'пара'}
Вывод
Traceback (most recent call last):
  File "example.py", line 1, in <module>
    d = {[1, 2]: 'пара'}
        ^^^^^^^^^^^^^^^^
TypeError: unhashable type: 'list'
Блок не запускается в браузерной песочнице: этот код падает. Путь к файлу зависит от того, как запущен скрипт, — суть в последней строке.

Почему так устроено: поиск по хешу

Словарь находит значение не перебором, а по хешу — числу, которое Python вычисляет из ключа и которое указывает, где лежит пара. Поиск по ключу быстрый именно поэтому: не надо сравнивать со всеми ключами подряд, хеш сразу называет полку. Теперь представьте, что ключом стал список и в него добавили элемент: содержимое изменилось, хеш должен стать другим, но пара осталась лежать на старой полке. Словарь не смог бы ни найти её, ни понять, куда она делась. Ключ обязан быть неизменяемым: всё, что может измениться после вставки, сломает поиск, поэтому список ключом быть не может.

frozenset: набор, который можно в ключ

дорога между городами — один ключ
roads = {
    frozenset({'А', 'Б'}): 12,
    frozenset({'Б', 'В'}): 7,
}

print(roads[frozenset({'А', 'Б'})])
print(roads[frozenset({'Б', 'А'})])
Вывод
12
12

Оба доступа вернули 12: frozenset({'А', 'Б'}) и frozenset({'Б', 'А'}) — один ключ, потому что множества не хранят порядка. Так делают словари дорог и групп: ключ — состав участников, «между А и Б лежит 12 километров», и неважно, в каком порядке города названы; значение — то, что этому набору принадлежит. Если состав нужно менять, frozenset не подойдёт — тогда ключ собирают кортежем из отсортированных элементов: tuple(sorted(cities)) — и тоже получают один ключ для одного набора.

Полный список: что годится в ключ

Правило одно — неизменяемость, — но на практике важно знать список целиком, а не только очевидные случаи. В ключ годится всё, что нельзя изменить: строки, числа, None, булевы значения, кортежи из таких значений и frozenset. Заглавные и строчные буквы в ключе — это разные ключи, а вот 1, 1.0 и True — один и тот же, потому что Python считает их равными.

пять видов ключей в одном словаре
d = {'строка': 1, 3: 'число', (1, 2): 'кортеж', None: 'ничего', True: 'истина'}

print(len(d))
print(d[None])
print(d[3])
Вывод
5
ничего
число

Пять пар в словаре, и d[None] работает: None — такое же неизменяемое значение, как и строка, хеш у него есть. С True чуть сложнее — он равен единице, поэтому если бы в словаре был ещё и ключ 1, один из них записался бы поверх другого. Дальше видно, почему это важно.

1, 1.0 и True — один ключ
d = {1: 'целое'}

d[1.0] = 'дробное'
d[True] = 'истина'

print(len(d))
print(d[1])
Вывод
1
истина

В словаре осталась одна пара, и под ключом 1 лежит 'истина'. Python сравнивает значения через ==, а 1 == 1.0 == True, поэтому для словаря все три — один и тот же ключ. Последняя запись перезаписала две предыдущие. Никакой ошибки: числа и булевы значения сравниваются между собой без проблем. Ловушка ждёт там, где ключи приходят из разных источников — из JSON приходят строки, из счётчика числа, и при смешивании один тип молча вытесняет другой.

Проверка: хешируемо или нет

Самый честный способ узнать, годится ли значение в ключ, — спросить у самого Python: функция hash() либо посчитает хеш, либо честно откажет. Для проверки типа удобнее collections.abc.Hashable: он говорит, можно ли значение положить в множество или словарь. Важная деталь: у списка атрибут __hash__ есть, но он равен None, поэтому проверка через hasattr врёт — пользуйтесь Hashable или пробуйте hash().

что можно, а что нельзя
from collections.abc import Hashable

print(isinstance('строка', Hashable))
print(isinstance((1, 2), Hashable))
print(isinstance(frozenset({1, 2}), Hashable))
print(isinstance([1, 2], Hashable))
print(isinstance({1, 2}, Hashable))
Вывод
True
True
True
False
False

Проверка отвечает за вас то, что иначе пришлось бы запоминать: строки, кортежи и замороженные множества — хешируемые, а список и обычное множество — нет. Проверка полезна в коде, который принимает ключи от пользователя или из конфига: if not isinstance(key, Hashable): raise TypeError(...) — и человек, положивший список в ключ, получит внятное сообщение вместо unhashable type: 'list' из глубины словаря.

Кортеж с изменяемым содержимым

Отдельный случай, который обманывает: кортеж сам по себе неизменяемый, но если внутри него список — всё равно unhashable. Хеш кортежа считается по хешам его элементов, а у списка хеша нет, поэтому портится весь кортеж целиком. Такое встречается в коде, который строит ключ из результата функции: список внутри кортежа — обычное дело, если забыть преобразовать его в tuple.

хеш не берётся
try:
    hash((1, [2]))
except TypeError as e:
    print('TypeError:', e)
Вывод
TypeError: unhashable type: 'list'

Сообщение указывает на list — на виновника внутри кортежа, а не на сам кортеж. Это сбивает с толку: кажется, что кортеж плохой, хотя кортежи в ключах работают. Лечится заменой списка на кортеж: (1, (2,)) — уже можно класть в ключ. Проверить заранее можно так: all(isinstance(part, Hashable) for part in key), если ключ собирается из кусков.

Свой класс в роли ключа

Иногда в ключ нужно положить не строку и не число, а объект: точку на карте, запись базы, пару пользователь и заказ. Python разрешит это, если класс умеет вычислить хеш и умеет сравниваться. Готовый рецепт — dataclass с frozen=True: он замораживает объект и сам пишет __hash__ и __eq__.

точка как ключ
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

grid = {Point(0, 0): 'начало', Point(2, 5): 'дерево'}

print(grid[Point(0, 0)])
print(len(grid))
Вывод
начало
2

Обратите внимание: grid[Point(0, 0)] находит значение, хотя создан новый объект Point(0, 0), а не тот, что клали в ключ. Сработало сравнение по полям: у замороженного dataclass __eq__ сравнивает значения, а __hash__ считается по тем же полям. Покадрово это удобно, пока не понадобится изменить точку — тогда объект придётся заменить целиком, потому что менять его нельзя.

Оговорка честная: для большинства задач такой класс — лишняя работа. Если в ключе достаточно двух чисел, кортеж (0, 0) справится, и писать dataclass ради него не надо. Класс оправдан там, где у сущности есть смысл: у точки есть координаты, у сотрудника — отдел и табельный номер, и «ключ из двух чисел» перестаёт что-то значить.

ТипВ ключ можно?Почему
strДаНеизменяема, хеш считается один раз
int, floatДаЧисла неизменяемы
bool, NoneДаНеизменяемые значения
tupleДа, если все части хешируемыХеш считается по хешам частей
frozensetДаНеизменяемое множество, порядок не важен
listНетИзменяемый — хеш «поедет»
setНетИзменяемый
dictНетИзменяемый
Свой классДа, если есть __hash__ и __eq__Dataclass(frozen=True) пишет их сам

Что дальше

Разобрались, что можно положить в ключ. Дальше — как словари сравнивать и копировать, чтобы не было сюрпризов: в уроке 19 про ==, is, copy и deepcopy. А если хочется собрать из ключей готовый отчёт — урок про сортировку словаря покажет, как получить упорядоченный список пар.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

d = {(1, 2): 'ok'}

print(d[(1, 2)])
d = {'x': 1}
marks = ['x']

d[marks] = 2
roads = {frozenset({'a', 'b'}): 1}

print(roads[frozenset({'b', 'a'})])
Проверь себя
0 / 8

1. Какие значения можно использовать как ключи словаря?

2. Что напечатает Python на d = {[1, 2]: 'пара'}?

3. Почему словарь не разрешает изменяемые ключи?

4. Кортеж (1, [2]) со списком внутри — годится в ключ?

5. Чем frozenset помогает при составных ключах?

6. Что произойдёт, если в словарь положить ключи 1, 1.0 и True?

7. Как надёжно проверить, годится ли значение в ключ словаря?

8. Как создать свой класс, который можно использовать как ключ словаря?

Карточки терминов
Запомнено: 0 / 10
Практика

Собери словарь points с кортежами-ключами: (0, 0) — 'начало', (2, 5) — 'дерево'. Выведи словарь целиком, затем достань и выведи оба значения по кортежам-ключам.

practice.py
Вопросы и ответы по уроку

Какие типы данных можно использовать в качестве ключей словаря Python?

Любые хешируемые, то есть неизменяемые: строки, числа (включая True и False), кортежи из хешируемых значений, frozenset. Списки, словари и множества ключами быть не могут — Python ответит TypeError: unhashable type.

Урок 3: KeyError и строгий доступ

Что значит ошибка TypeError: unhashable type: 'list'?

Вы пытаетесь использовать список как ключ словаря — при создании пары или записи значения. У списка нет хеша, а без него словарь не может определить место для пары. Замените список кортежем: вместо [1, 2] запишите (1, 2) — и ключ заработает.

Как использовать два значения в роли одного ключа?

Кортежем: d[(10, 20)] = 'точка' — координаты или пара полей собираются в один ключ. Если порядок элементов не важен (дорога между городами, группа людей), берите frozenset — тогда перестановка элементов даёт тот же ключ.

Почему кортеж со списком внутри тоже нельзя в ключ?

Хешируемость кортежа зависит от содержимого: список внутри можно изменить, значит и хеш такого кортежа мог бы измениться. Python честно откажет: TypeError: unhashable type: 'list'. Хешируемыми должны быть все части ключа без исключения.

Можно ли использовать None в качестве ключа словаря?

Да — None неизменяемое значение с постоянным хешем, так что d[None] работает. Так же годится True и False, но помните: True равен 1, а 1 равно 1.0, поэтому эти три ключа для словаря — один и тот же.

Как проверить в коде, что значение можно использовать как ключ?

Через isinstance(value, Hashable) из collections.abc — так возвращается True для строк, чисел, кортежей и frozenset, и False для списков, множеств и словарей. Проверка через hasattr(value, '__hash__') врёт: у списка этот атрибут есть, но он равен None.

Можно ли использовать свой класс как ключ словаря?

Да, если класс умеет вычислить хеш и сравниваться: нужны __hash__ и __eq__. Проще всего взять dataclass с frozen=True — он пишет оба метода по полям сам. Обычный класс без этих методов использует хеш по id, и два равных по смыслу объекта окажутся разными ключами.

Почему 1, 1.0 и True — один ключ словаря?

Python сравнивает ключи через оператор ==, а 1 == 1.0 == True. Все три записи претендуют на один ключ, и выживает последняя. Поэтому ключи из разных источников — числа из счётчика и строки из JSON — приводят к одному типу явно: {str(k): v for k, v in raw.items()}.

Понравился урок? Сошлитесь на него

«Ключ обязан быть неизменяемым: всё, что может измениться после вставки, сломает поиск, поэтому список ключом быть не может.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.