Недавно мы с подписчиками моего ТГ-канала спорили по поводу, казалось бы, совсем простого вопроса – элементы каких типов данных могут быть ключами словаря. Во многих статьях в сети написано, что это объект любого неизменяемого типа данных, например, числа, строки, кортежа. Т.е. ключ словаря – всегда объект неизменяемого типа данных. Но так ли это? А что насчет ключей в виде объектов пользовательских именных функций или экземпляров пользовательских классов. Конечно, мы вряд ли будем создавать такие ключи, но все же их можно использовать или нет?

Проверим:

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)


  1. CrazyElf
    09.06.2025 11:29

    Формально нужна хэшируемость и сравнимость объектов, но по факту проще запомнить не изменяемость. Проблема с изменяемыми объектами в том, что у них при изменении меняется либо хэш, либо значение и вы уже не сможете нормально найти ключ в словаре, который ранее был задан таким объектом. Они могут и не измениться, но гарантии нет. Поэтому, от греха подальше, изменяемым объектам стараются просто не делать доступной функцию хэширования, чтобы не было возможности их использовать в качестве ключа для словаря и собирать грабли после этого.

    Ну а когда вы объявляете свой класс, а не пользуетесь готовым, питон за вас заранее не знает, что у вас там будет с изменяемостью, поэтому вы сами должны проследить за тем, будет ли у вашего класса функция хэширования и если да, то вы должны сделать её адекватной и сообразной функции сравнения.


    1. advanced_python_expert Автор
      09.06.2025 11:29

      все верно, для упрощения говорим, что ключи - неизменяемые типы данных, вряд ли ключами словаря будут объекты функций и экземпляры классов, но нужно упор делать на хешируемость


  1. eandr_67
    09.06.2025 11:29

    Вы относите слово "неизменяемый" к содержимому структуры данных, но применительно к ключам ассоциативного массива (в Python - "словаря") смотреть надо не на неизменяемость содержимого, а на неизменяемость того, что хэшируется и/или сравнивается операциями == и !=.

    В случае объектов хэшируется/сравнивается не содержимое объектов, а никак не изменяемые указатели (адрес в памяти, id в таблице объектов и т.п.) этих объектов. Независимо от того, как меняются значения полей объекта, указатель и хэш, полученный из этого указателя, остаются неизменными. Аналогично с функциями.


  1. AbitLogic
    09.06.2025 11:29

    "зачастую на интернет-ресурсах про ключи говорят как про объекты неизменяемых типов данных,"

    Не знаю где так говорят, если читать книги по Rust и С++ там сказано если ты можешь захешировать свой тип - вперёд


    1. advanced_python_expert Автор
      09.06.2025 11:29

      здесь в контексте Python


  1. avshkol
    09.06.2025 11:29

    dct = {obj_1: 1, obj_2: 2}

    Хм, интересная конструкция: мы присваивает каждому объекту значение, например, рейтинг, при этом не внутри объекта, т.е. ни объект, ни класс об этом "не знают". Где бы это могло пригодиться? [Дай 10 вариантов] - строка, отделяющая теперешнее время от времени ещё несколько лет назад...

    Ответ deepseek, который ещё несколько лет назад был бы немыслимо ждать от машины:

    Отличный вопрос! Хранение внешних атрибутов объектов в словаре — мощный паттерн, особенно когда нельзя или нежелательно модифицировать сами классы объектов. Вот 10 практических применений:

    1. Кэширование результатов вычислений:

      • cache = {obj: expensive_calculation(obj) for obj in objects}

      • Пригодится: Для тяжёлых функций (рендеринг, сложные математические операции). Избегаем повторных вычислений для одного объекта.

    2. Временные состояния/флаги:

      • selected_objects = {obj_1: True, obj_2: False}

      • Пригодится: UI (выделение элементов), обработка событий ("уже обработан"), временные отметки в алгоритмах (обход графа - visited[node] = True).

    3. Связывание метаданных:

      • object_metadata = {obj: {"last_accessed": timestamp, "owner": user_id}}

      • Пригодится: Системы кеширования (время доступа), аудит (кто последний менял), контекстная информация для обработки.

    4. Подсчёт частоты / Агрегация:

      • word_count = {}; for word in words: word_count[word] = word_count.get(word, 0) + 1

      • Пригодится: Анализ текста (частотность слов), подсчёт событий (сколько раз пользователь выполнил действие), поиск дубликатов.

    5. Сопоставление объектов с внешними ресурсами:

      • texture_mapping = {game_object: texture_id}

      • Пригодится: Графика (связь модели с текстурой), базы данных (связь объекта ORM с ID записи), сетевые соединения (связь пользователя с сокетом).

    6. Приоритизация / Очереди:

      • priority_queue = []; heapq.heappush(priority_queue, (priority_dict[task], task))

      • Пригодится: Планировщики задач (приоритет задачи не свойство самой задачи, а определяется контекстом), алгоритмы поиска пути (A* - f_score[node] = ...).

    7. Вес рёбер / Сила связей (Графы):

      • graph[from_node] = {to_node: weight, ...}

      • Пригодится: Представление взвешенных графов (социальные сети - сила связи, карты - расстояние/время проезда). Вес - атрибут связи, а не узла.

    8. Внешняя конфигурация / Настройки:

      • object_settings = {widget: {"color": "red", "size": 10}}

      • Пригодится: GUI (стилизация элементов, которые не хранят стиль внутри), управление поведением плагинов. Настройки задаются извне.

    9. Группировка / Классификация:

      • groups = {"admin": [user1, user2], "user": [user3]}

      • Пригодится: Системы прав доступа (группы пользователей), категоризация элементов (товары в категориях). Группа - внешний контейнер.

    10. Хранение сессионных данных:

      • session_store = {session_id: {"user": user_obj, "cart": cart_items}}

      • Пригодится: Веб-приложения. Данные сессии (корзина покупок, состояние) привязаны к идентификатору сессии, а не к объекту пользователя напрямую (особенно если пользователь не залогинен).

    Ключевые преимущества такого подхода:

    • Неинвазивность: Не нужно изменять исходные классы объектов.

    • Гибкость: Легко добавлять, изменять и удалять атрибуты для объектов динамически.

    • Разделение ответственности: Данные, специфичные для контекста использования, хранятся отдельно от базовых данных объекта.

    • Эффективность: Быстрый (O(1) в среднем) доступ к данным по ключу-объекту.

    • Работа с "чужими" объектами: Можно прикреплять данные к объектам из библиотек, код которых вы не контролируете.

    Важные замечания:

    • Хешируемость: Ключи словаря (ваши объекты) должны быть хешируемыми (в Python обычно реализуют hash и eq).

    • Сравнение по ссылке: По умолчанию в Python объекты сравниваются по идентификатору (адресу в памяти). Это часто ожидаемо в таких сценариях.

    • Жизненный цикл: Словарь хранит сильные ссылки на объекты-ключи. Это может препятствовать сборке мусора, если объект больше нигде не используется, но остался в словаре. Для временных данных используйте weakref.WeakKeyDictionary.

    Этот паттерн — отличный инструмент для случаев, когда данные контекстно-зависимы или временны, и их не стоит "зашивать" в саму сущность объекта.