← Назад к вопросам
Почему нельзя сделать ключом словаря изменяемую структуру данных в Python?
2.0 Middle🔥 201 комментариев
#Python Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Почему нельзя использовать изменяемые структуры как ключи словаря
Ключевой момент: хеширование и неизменяемость
Словари в Python используют хеш-таблицы для быстрого поиска. Каждый ключ хешируется в индекс массива. Если ключ изменится, его хеш изменится, и словарь не сможет найти значение.
Требование: ключ должен быть hashable
# Hashable объекты (неизменяемые) — подходят как ключи
dict_key = {
42: "число", # int ✅
"name": "строка", # str ✅
(1, 2, 3): "кортеж", # tuple ✅
True: "булев", # bool ✅
}
# Non-hashable объекты (изменяемые) — НЕ подходят
try:
bad_key = {[1, 2, 3]: "значение"} # list ❌
except TypeError as e:
print(e) # unhashable type: 'list'
try:
bad_key = {{"a": 1}: "значение"} # dict ❌
except TypeError as e:
print(e) # unhashable type: 'dict'
try:
bad_key = {{1, 2, 3}: "значение"} # set ❌
except TypeError as e:
print(e) # unhashable type: 'set'
Почему изменяемость проблема: хеш нарушается
Сценарий проблемы
# Представим, если бы мы МОГЛИ использовать список как ключ
key = [1, 2, 3]
my_dict = {key: "value"}
# Хеш ключа в момент создания: hash([1, 2, 3]) = H1
# Индекс в таблице: H1 % table_size = 5
# my_dict[5] → "value"
print(my_dict[key]) # Находим: "value" ✅
# Но потом кто-то изменяет ключ
key[1] = 999 # Меняем список на [1, 999, 3]
# Теперь хеш другой: hash([1, 999, 3]) = H2
# Новый индекс: H2 % table_size = 8
# my_dict[8] → None
print(my_dict[key]) # Не находим! ❌
# KeyError или возвращает значение из позиции 8
Внутренний механизм: хеш-функция
# Неизменяемые объекты имеют СТАБИЛЬНЫЙ хеш
a = (1, 2, 3)
print(hash(a)) # 529344067295497451
print(hash(a)) # 529344067295497451 (ОДИНАКОВЫЙ!)
# Изменяемые объекты НЕ имеют хеш
my_list = [1, 2, 3]
print(hash(my_list)) # TypeError: unhashable type: 'list'
# Почему? Потому что Python не может гарантировать,
# что хеш останется таким же после изменения
Как Python проверяет, является ли объект hashable
class MyImmutableClass:
"""Собственный неизменяемый класс"""
def __init__(self, value):
self.value = value
def __hash__(self):
"""Метод __hash__ делает объект hashable"""
return hash(self.value)
def __eq__(self, other):
"""Нужен для сравнения"""
return isinstance(other, MyImmutableClass) and self.value == other.value
# Теперь можно использовать как ключ
obj = MyImmutableClass(42)
my_dict = {obj: "значение"} # ✅ Работает!
class MyMutableClass:
"""Мутируемый класс — НЕЛЬЗЯ использовать как ключ"""
def __init__(self, value):
self.value = value
# Если явно устанавливаем __hash__ = None,
# то объект становится non-hashable
__hash__ = None
# Теперь НЕЛЬЗЯ использовать как ключ
obj = MyMutableClass(42)
try:
my_dict = {obj: "значение"} # ❌
except TypeError as e:
print(e) # unhashable type: 'MyMutableClass'
Правило: если переопределяешь eq, нужен hash
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
"""Переопределили сравнение"""
return self.x == other.x and self.y == other.y
# ВАЖНО: если переопределяем __eq__,
# нужно либо переопределить __hash__,
# либо явно запретить (установить = None)
def __hash__(self):
"""Теперь это hashable"""
return hash((self.x, self.y))
point = Point(1, 2)
my_dict = {point: "координата"} # ✅ Работает
# Но если бы мы НЕ определили __hash__:
class BadPoint:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
# __hash__ не определён
# В Python 3 это автоматически устанавливает __hash__ = None
bad_point = BadPoint(1, 2)
try:
my_dict = {bad_point: "координата"} # ❌
except TypeError:
print("Cannot use BadPoint as dict key")
Практический пример: почему это критично
# ПРОБЛЕМА: использование списка (если бы было возможно)
users = {}
user_id = [123] # Если бы можно было использовать как ключ
users[user_id] = {"name": "Alice", "age": 30}
print(users[user_id]) # {"name": "Alice", "age": 30}
# Кто-то случайно изменяет список
user_id[0] = 456
# Теперь не можем найти!
print(user_id in users) # False ❌
print(users.get(user_id)) # None ❌
# Данные потеряны!
Решение: использовать кортежи
# ПРАВИЛЬНО: использовать кортеж (неизменяемый)
users = {}
user_id = (123,) # Кортеж, неизменяемый ✅
users[user_id] = {"name": "Alice", "age": 30}
print(users[user_id]) # {"name": "Alice", "age": 30}
# Даже если попытаться изменить, это просто создаст новый объект
user_id = (456,) # Это новый кортеж, старый не изменился
print(user_id in users) # False (это другой ключ)
print((123,) in users) # True (оригинальный ключ все ещё там)
# Более сложный пример с вложенными кортежами
coordinates = {
(0, 0): "origin",
(1, 2): "point1",
((1, 2), (3, 4)): "line", # Кортеж из кортежей!
}
Как работает хеш-таблица словаря
# Внутренний процесс:
class SimpleDictImpl:
def __init__(self):
self.table = [None] * 16 # Начальный размер
self.size = 0
def put(self, key, value):
# 1. Вычисляем хеш ключа
hash_value = hash(key)
# 2. Вычисляем индекс в таблице
index = hash_value % len(self.table)
# 3. Сохраняем (с обработкой коллизий)
self.table[index] = (key, value)
self.size += 1
def get(self, key):
# 1. Вычисляем хеш ключа (ДОЛЖЕН БЫТЬ СТАБИЛЬНЫМ!)
hash_value = hash(key)
# 2. Вычисляем индекс
index = hash_value % len(self.table)
# 3. Получаем значение
if self.table[index] is not None:
stored_key, value = self.table[index]
if stored_key == key: # Проверяем равенство
return value
return None
# Если ключ изменился между put и get, хеши будут разные!
# Поэтому ключ ДОЛЖЕН быть неизменяемым
Почему Python требует неизменяемость: причины
1. Производительность
# С изменяемыми ключами пришлось бы:
# - Каждый раз перехешировать ключ при поиске
# - Проверять, изменился ли ключ
# - Перестраивать таблицу
# Это замедлило бы поиск со O(1) на O(n)
2. Корректность
# Если бы позволили изменяемые ключи,
# словарь мог бы потерять данные:
my_dict = {}
key = [1, 2, 3]
my_dict[key] = "value"
key.append(4) # Изменяем ключ
# Теперь данные потеряны или доступны неправильно
3. Семантика языка
# Договор: если x == y и x hashable, то hash(x) == hash(y)
# Это условие может быть нарушено с изменяемыми объектами
a = [1, 2]
b = [1, 2]
# a == b (True), но если бы были ключами, нарушилось бы свойство
Что можно использовать как ключи
# ✅ МОЖНО (неизменяемые)
valid_keys = {
None: "null",
True: "boolean",
42: "integer",
3.14: "float",
"string": "text",
b"bytes": "binary",
(1, 2, "nested"): "tuple",
frozenset([1, 2, 3]): "frozenset",
}
# ❌ НЕЛЬЗЯ (изменяемые)
invalid = {
[1, 2]: "list", # TypeError
{1, 2}: "set", # TypeError
{"a": 1}: "dict", # TypeError
bytearray(b"data"): "ba", # TypeError
}
Заключение
Нельзя использовать изменяемые структуры как ключи словаря, потому что:
- Хеш должен быть стабильным: неизменяемость гарантирует, что хеш не изменится
- Быстрый поиск: O(1) поиск требует стабильного хеша
- Корректность: изменение ключа приведёт к потере данных
- Семантика: ключ определяет позицию в хеш-таблице, он не может меняться
Используйте неизменяемые типы (кортежи, frozenset) для создания ключей из сложных структур.