Ключи: что можно и что нельзя
Ключом бывают строки, числа и кортежи, но не список: 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, один из них записался бы поверх другого. Дальше видно, почему это важно.
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'})])
1. Какие значения можно использовать как ключи словаря?
2. Что напечатает Python на d = {[1, 2]: 'пара'}?
3. Почему словарь не разрешает изменяемые ключи?
4. Кортеж (1, [2]) со списком внутри — годится в ключ?
5. Чем frozenset помогает при составных ключах?
6. Что произойдёт, если в словарь положить ключи 1, 1.0 и True?
7. Как надёжно проверить, годится ли значение в ключ словаря?
8. Как создать свой класс, который можно использовать как ключ словаря?
Собери словарь points с кортежами-ключами: (0, 0) — 'начало', (2, 5) — 'дерево'. Выведи словарь целиком, затем достань и выведи оба значения по кортежам-ключам.
Какие типы данных можно использовать в качестве ключей словаря 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-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Словари · Урок 17
Словарь как данные: разбор ответа API
Ответ API — это вложенный словарь: достаём поля по цепочке ключей, читаем безопасно через get с умолчанием, разбираем списки записей и собираем из ответа готовый отчёт.
Словари · Урок 19
Сравнение словарей и копирование
Словари сравниваются по парам — порядок не важен, а копировать их нужно аккуратно: copy() делит вложенные значения, deepcopy копирует насквозь.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Множества · Урок 17
Множества и словари Python: hash и общие механизмы
Множество и словарь построены на одном механизме: hash назначает каждому элементу полку, и проверка in становится мгновенной. Ключи словаря — готовое множество, над которым работают & и |.
ключи словаря как множество pythonunhashable type python
Множества · Урок 10
frozenset: неизменяемое множество в Python
Множество, которое нельзя менять: frozenset создаётся один раз, поддерживает все проверки и операции чтения и годится туда, куда set не пускают — ключом в словарь и элементом другого множества.
frozenset pythonpython frozenset
Словари · Урок 7
in: проверка ключа в словаре
Оператор in отвечает на вопрос «есть ли такой ключ»: проверка перед доступом вместо KeyError, not in, грабли с проверкой значений и причина скорости поиска.
проверить есть ли ключ в словареоператор in python словарь