Недавно мы с подписчиками моего ТГ-канала спорили по поводу, казалось бы, совсем простого вопроса – элементы каких типов данных могут быть ключами словаря. Во многих статьях в сети написано, что это объект любого неизменяемого типа данных, например, числа, строки, кортежа. Т.е. ключ словаря – всегда объект неизменяемого типа данных. Но так ли это? А что насчет ключей в виде объектов пользовательских именных функций или экземпляров пользовательских классов. Конечно, мы вряд ли будем создавать такие ключи, но все же их можно использовать или нет?
Проверим:
class MyClass:
pass
obj_1 = MyClass()
obj_2 = MyClass()
dct = {obj_1: 1, obj_2: 2}
print(dct[obj_1])
def func():
pass
dct = {func: 1}
print(dct[func])
Ошибок нет. Получается, что может экземпляр класса и объект пользовательской функции быть ключом! Но ведь это изменяемые типы данных. И это несложно проверить. Если объект относится к изменяемому типу данных, то при попытке изменения id объекта должен остаться прежним.
Вот числа, они неизменяемые:
x = 10
y = x
print(id(x), id(y))
x = x + 1
print(id(x), id(y))
Результат:

При «изменении» значения переменной x мы получили совсем новый объект.
Теперь то же самое повторим на списках:
x = [1, 2, 3]
y = x
print(id(x), id(y))
x.append(4)
print(id(x), id(y))
Результат:

Идентификатор не изменился, т.е. имеем объект изменяемого типа данных.
Теперь посмотрим, что с изменяемостью у экземпляров пользовательских классов:
class User:
def __init__(self, age):
self.age = age
u = User(12)
b = u
print(id(u), id(b))
u.age = 40
print(id(u), id(b))
print(b.age)
Результат:

Получается экземпляры пользовательских классов – изменяемые.
А что с объектами функций? Можем мы у объекта существующей функции изменить логику программным путем? Пример ниже показывает, что можем, т.к. id объекта функции не изменился:
def func():
return "hello"
print(id(func))
def other(a, b):
return a + b
func.__code__ = other.__code__
print(id(func))
Результат:

И это все правильно. Объекты изменяемых типов данных при изменениях сохраняют свой id. Об этом указано в документации.
Значит изменяемость здесь совсем не при чем? В чем же тогда дело и как понять объекты каких классов могут быть ключами словаря? Обратимся к теории.
Словарь, это хеш-таблица, т.е. ключи должны быть объектами классов, у которых существует метод hash. В Python пользовательские функции и классы по умолчанию хешируются, а значит в качестве ключей использоваться могут, но при этом они изменяемые.
P.S. Теперь нужно понять, почему зачастую на интернет-ресурсах про ключи говорят как про объекты неизменяемых типов данных, хотя нужно делать упор не на изменяемость, а на хешируемость. А что вы думаете об этом?
Комментарии (6)
eandr_67
09.06.2025 11:29Вы относите слово "неизменяемый" к содержимому структуры данных, но применительно к ключам ассоциативного массива (в Python - "словаря") смотреть надо не на неизменяемость содержимого, а на неизменяемость того, что хэшируется и/или сравнивается операциями == и !=.
В случае объектов хэшируется/сравнивается не содержимое объектов, а никак не изменяемые указатели (адрес в памяти, id в таблице объектов и т.п.) этих объектов. Независимо от того, как меняются значения полей объекта, указатель и хэш, полученный из этого указателя, остаются неизменными. Аналогично с функциями.
AbitLogic
09.06.2025 11:29"зачастую на интернет-ресурсах про ключи говорят как про объекты неизменяемых типов данных,"
Не знаю где так говорят, если читать книги по Rust и С++ там сказано если ты можешь захешировать свой тип - вперёд
avshkol
09.06.2025 11:29dct = {obj_1: 1, obj_2: 2}
Хм, интересная конструкция: мы присваивает каждому объекту значение, например, рейтинг, при этом не внутри объекта, т.е. ни объект, ни класс об этом "не знают". Где бы это могло пригодиться? [Дай 10 вариантов] - строка, отделяющая теперешнее время от времени ещё несколько лет назад...
Ответ deepseek, который ещё несколько лет назад был бы немыслимо ждать от машины:
Отличный вопрос! Хранение внешних атрибутов объектов в словаре — мощный паттерн, особенно когда нельзя или нежелательно модифицировать сами классы объектов. Вот 10 практических применений:
-
Кэширование результатов вычислений:
cache = {obj: expensive_calculation(obj) for obj in objects}
Пригодится: Для тяжёлых функций (рендеринг, сложные математические операции). Избегаем повторных вычислений для одного объекта.
-
Временные состояния/флаги:
selected_objects = {obj_1: True, obj_2: False}
Пригодится: UI (выделение элементов), обработка событий ("уже обработан"), временные отметки в алгоритмах (обход графа -
visited[node] = True
).
-
Связывание метаданных:
object_metadata = {obj: {"last_accessed": timestamp, "owner": user_id}}
Пригодится: Системы кеширования (время доступа), аудит (кто последний менял), контекстная информация для обработки.
-
Подсчёт частоты / Агрегация:
word_count = {}; for word in words: word_count[word] = word_count.get(word, 0) + 1
Пригодится: Анализ текста (частотность слов), подсчёт событий (сколько раз пользователь выполнил действие), поиск дубликатов.
-
Сопоставление объектов с внешними ресурсами:
texture_mapping = {game_object: texture_id}
Пригодится: Графика (связь модели с текстурой), базы данных (связь объекта ORM с ID записи), сетевые соединения (связь пользователя с сокетом).
-
Приоритизация / Очереди:
priority_queue = []; heapq.heappush(priority_queue, (priority_dict[task], task))
Пригодится: Планировщики задач (приоритет задачи не свойство самой задачи, а определяется контекстом), алгоритмы поиска пути (A* -
f_score[node] = ...
).
-
Вес рёбер / Сила связей (Графы):
graph[from_node] = {to_node: weight, ...}
Пригодится: Представление взвешенных графов (социальные сети - сила связи, карты - расстояние/время проезда). Вес - атрибут связи, а не узла.
-
Внешняя конфигурация / Настройки:
object_settings = {widget: {"color": "red", "size": 10}}
Пригодится: GUI (стилизация элементов, которые не хранят стиль внутри), управление поведением плагинов. Настройки задаются извне.
-
Группировка / Классификация:
groups = {"admin": [user1, user2], "user": [user3]}
Пригодится: Системы прав доступа (группы пользователей), категоризация элементов (товары в категориях). Группа - внешний контейнер.
-
Хранение сессионных данных:
session_store = {session_id: {"user": user_obj, "cart": cart_items}}
Пригодится: Веб-приложения. Данные сессии (корзина покупок, состояние) привязаны к идентификатору сессии, а не к объекту пользователя напрямую (особенно если пользователь не залогинен).
Ключевые преимущества такого подхода:
Неинвазивность: Не нужно изменять исходные классы объектов.
Гибкость: Легко добавлять, изменять и удалять атрибуты для объектов динамически.
Разделение ответственности: Данные, специфичные для контекста использования, хранятся отдельно от базовых данных объекта.
Эффективность: Быстрый (O(1) в среднем) доступ к данным по ключу-объекту.
Работа с "чужими" объектами: Можно прикреплять данные к объектам из библиотек, код которых вы не контролируете.
Важные замечания:
Хешируемость: Ключи словаря (ваши объекты) должны быть хешируемыми (в Python обычно реализуют
hash
иeq
).Сравнение по ссылке: По умолчанию в Python объекты сравниваются по идентификатору (адресу в памяти). Это часто ожидаемо в таких сценариях.
Жизненный цикл: Словарь хранит сильные ссылки на объекты-ключи. Это может препятствовать сборке мусора, если объект больше нигде не используется, но остался в словаре. Для временных данных используйте
weakref.WeakKeyDictionary
.
Этот паттерн — отличный инструмент для случаев, когда данные контекстно-зависимы или временны, и их не стоит "зашивать" в саму сущность объекта.
-
CrazyElf
Формально нужна хэшируемость и сравнимость объектов, но по факту проще запомнить не изменяемость. Проблема с изменяемыми объектами в том, что у них при изменении меняется либо хэш, либо значение и вы уже не сможете нормально найти ключ в словаре, который ранее был задан таким объектом. Они могут и не измениться, но гарантии нет. Поэтому, от греха подальше, изменяемым объектам стараются просто не делать доступной функцию хэширования, чтобы не было возможности их использовать в качестве ключа для словаря и собирать грабли после этого.
Ну а когда вы объявляете свой класс, а не пользуетесь готовым, питон за вас заранее не знает, что у вас там будет с изменяемостью, поэтому вы сами должны проследить за тем, будет ли у вашего класса функция хэширования и если да, то вы должны сделать её адекватной и сообразной функции сравнения.
advanced_python_expert Автор
все верно, для упрощения говорим, что ключи - неизменяемые типы данных, вряд ли ключами словаря будут объекты функций и экземпляры классов, но нужно упор делать на хешируемость