Какие типы данных в Python могут быть ключом в словаре (dict)?

Ответ

В Python ключом словаря может быть любой хешируемый (hashable) объект. Хешируемость означает, что объект имеет неизменяемое значение хеша на протяжении всей его жизни и может быть сравнен с другими объектами (имеет методы __hash__() и __eq__()).

Основные хешируемые (допустимые) типы:

  • Неизменяемые встроенные типы: int, float, str, bytes, tuple, frozenset, bool.
  • Пользовательские классы: Экземпляры пользовательских классов по умолчанию хешируемы (хеш основан на id()). Однако, если вы переопределите __eq__(), то должны также явно определить __hash__() для сохранения хешируемости.

Нехешируемые (недопустимые) типы:

  • Изменяемые коллекции: list, set, dict.
  • Массивы NumPy и другие изменяемые объекты.

Почему это важно? Словарь внутри реализован как хеш-таблица. При вставке пары ключ-значение вычисляется хеш ключа, который определяет "ячейку" для быстрого поиска. Если бы ключ мог измениться, его хеш тоже изменился бы, и найти связанное с ним значение стало бы невозможно.

Примеры:

# Допустимые ключи
valid_dict = {
    42: 'integer',
    3.14: 'float',
    'hello': 'string',
    (1, 2, 3): 'tuple of integers',
    ('a', ('b', 'c')): 'nested tuple',
    frozenset([1, 2]): 'frozenset',
    True: 'boolean (подтип int)',
    MyImmutableClass(): 'пользовательский класс'
}

# Вызовет TypeError: unhashable type
invalid_dict = {
    [1, 2]: 'list',  # НЕЛЬЗЯ
    {'a': 1}: 'dict', # НЕЛЬЗЯ
    {1, 2}: 'set'     # НЕЛЬЗЯ
}

# Кортеж может быть ключом, только если все его элементы хешируемы
good_tuple_key = (1, 'a', (2, 3))  # ОК
bad_tuple_key = (1, [2, 3])       # TypeError, т.к. содержит список

Ответ 18+ 🔞

А, слушай, вот тебе на самом деле важная штука, которую многие на первых порах проёбывают. Речь про то, что можно, а что нельзя пихать в ключи словаря в Python. Запомни раз и навсегда, чтобы потом не охуевать от ошибок.

Короче, ключом может быть только хешируемый объект. Что это значит на нормальном языке? Это значит, что у объекта есть какое-то неизменное, постоянное значение хеша, которое не меняется, пока объект живёт, и его можно сравнивать с другими. Технически — у него должны быть методы __hash__() и __eq__(). Если этого нет — забудь, это не ключ.

Что можно пихать в ключи (хешируемое):

  • Всё неизменяемое и встроенное: int, float, str, bytes, tuple, frozenset, bool. Да, True и False — это, по сути, те же самые единичка и ноль, так что они тоже катят.
  • Свои классы: Да, экземпляры твоих собственных классов по умолчанию тоже хешируемы. Но тут есть подвох, ёпта! Если ты вдруг начнёшь умничать и переопределишь метод __eq__() для сравнения, то ты обязан явно прописать и __hash__(), иначе всё накроется медным тазом и хешируемость потеряется. Не забывай об этом, а то будет тебе хиросима.

Что НЕЛЬЗЯ пихать в ключи (нехешируемое):

  • Всё изменяемое: list, set, dict. Вот эти ребята — распиздяи, их внутренности могут меняться, поэтому они не годятся.
  • Массивы NumPy и прочая изменяемая хрень.

А почему, собственно, нельзя? Да потому что словарь внутри — это хеш-таблица, ядрёна вошь! Когда ты добавляешь пару, Python быстренько вычисляет хеш от ключа, чтобы понять, в какую ячейку положить значение для потом быстрого поиска. Представь, что ты положил список как ключ, а потом его изменил. Его хеш поменяется, и как потом искать значение, которое ты к нему привязал? Да никак! Поэтому Python просто не даёт тебе такой возможности, чтобы не было пиздеца.

Примеры, чтобы совсем понятно стало:

# Вот это всё работает — кладём спокойно
valid_dict = {
    42: 'integer',
    3.14: 'float',
    'hello': 'string',
    (1, 2, 3): 'tuple of integers',
    ('a', ('b', 'c')): 'nested tuple',
    frozenset([1, 2]): 'frozenset',
    True: 'boolean (подтип int)',
    MyImmutableClass(): 'пользовательский класс'
}

# А вот это вызовет TypeError: unhashable type — готовься к ошибке
invalid_dict = {
    [1, 2]: 'list',  # НИНАДА
    {'a': 1}: 'dict', # АБСОЛЮТНО НИНАДА
    {1, 2}: 'set'     # ЗАБУДЬ
}

# С кортежами отдельная история. Сам кортеж неизменяемый, но он может быть ключом ТОЛЬКО если ВСЕ его элементы тоже хешируемы.
good_tuple_key = (1, 'a', (2, 3))  # ОК, всё неизменяемое внутри
bad_tuple_key = (1, [2, 3])       # Ошибка! Потому что внутри сидит изменяемый список, пидарас шерстяной.

Вот и вся магия. Главное — запомни принцип: ключ должен быть неизменным. Всё, что может поменяться после создания, — это манда с ушами для ключа словаря. Доверия к таким объектам — ноль ебать.