← Назад к вопросам

Что может быть ключом в словаре?

1.3 Junior🔥 91 комментариев

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Ключи в словаре Python: полное руководство

Краткий ответ

Ключом может быть любой immutable (неизменяемый) тип данных:

  • int, float, str, tuple, bool, frozenset, None
  • НЕ может быть: list, dict, set (изменяемые типы)

Почему только immutable?

Основная идея: хеширование

Диктионарь в Python использует hash table для быстрого поиска:

# Когда ты пишешь
my_dict = {'key': 'value'}

# Под капотом Python делает:
hash_value = hash('key')  # вычисляет хеш строки
bucket = hash_value % table_size  # определяет позицию в таблице
my_dict[bucket] = value

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

# Это НЕ сработает
my_list = [1, 2, 3]
my_dict = {my_list: 'value'}  # TypeError: unhashable type: 'list'

# Почему? Если список изменится, его хеш изменится
my_list[0] = 999  # теперь список другой
# Python не сможет найти 'value' в словаре

Примеры: что может быть ключом

1. Строки (str)

user_data = {
    'john_doe': {'age': 30, 'city': 'Moscow'},
    'jane_smith': {'age': 25, 'city': 'SPB'}
}

print(user_data['john_doe'])  # {'age': 30, 'city': 'Moscow'}

2. Числа (int, float)

# int как ключ
temp_by_hour = {
    0: -5.2,   # 00:00 - минус 5.2°
    1: -4.8,   # 01:00
    2: -3.1,   # 02:00
}

# float как ключ (редко, но возможно)
prices = {
    0.5: 'half',
    1.0: 'one',
    1.5: 'one and half'
}

print(prices[0.5])  # 'half'

3. Кортежи (tuple)

Очень полезно для многомерных ключей:

# Координаты на карте
map_data = {
    (0, 0): 'start',
    (10, 10): 'end',
    (5, 5): 'checkpoint'
}

print(map_data[(5, 5)])  # 'checkpoint'

# Составной ключ: (город, улица, дом)
addresses = {
    ('Moscow', 'Red Square', 1): 'Kremlin',
    ('SPB', 'Nevsky', 25): 'Shop'
}

for (city, street, number), place in addresses.items():
    print(f"{city}, {street}, {number}: {place}")

4. Булевы значения (bool)

feature_flags = {
    True: 'enabled',
    False: 'disabled'
}

print(feature_flags[True])  # 'enabled'

# На самом деле bool — это подкласс int
print(True == 1)   # True
print(False == 0)  # True

5. None

defaults = {
    None: 'undefined',
    'name': 'John',
    'age': 30
}

print(defaults[None])  # 'undefined'

6. frozenset

Неизменяемое множество:

# frozenset в качестве ключа
groups = {
    frozenset(['Alice', 'Bob']): 'Team A',
    frozenset(['Charlie', 'Diana']): 'Team B',
    frozenset(['Eve']): 'Solo'
}

team_key = frozenset(['Alice', 'Bob'])
print(groups[team_key])  # 'Team A'

Примеры: ЧТО НЕ может быть ключом

❌ Список (list)

# Ошибка
my_dict = {[1, 2, 3]: 'value'}
# TypeError: unhashable type: 'list'

# Почему нельзя?
my_list = [1, 2, 3]
my_list.append(4)  # изменили
# Теперь это другой список, хеш изменился

❌ Словарь (dict)

# Ошибка
my_dict = {{'key': 'val'}: 'value'}
# TypeError: unhashable type: 'dict'

❌ Множество (set)

# Ошибка
my_dict = {{1, 2, 3}: 'value'}
# TypeError: unhashable type: 'set'

# Решение: используй frozenset
my_dict = {frozenset([1, 2, 3]): 'value'}  # OK

Практические паттерны

Паттерн 1: Кэширование с tuple ключами

from functools import lru_cache

@lru_cache(maxsize=128)
def get_distance(point1, point2):
    """Вычисляем расстояние (кэшируется)"""
    x1, y1 = point1
    x2, y2 = point2
    return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5

# lru_cache требует hashable аргументы
result = get_distance((0, 0), (3, 4))  # 5.0
result = get_distance((0, 0), (3, 4))  # берется из кэша

Паттерн 2: Частотный анализ

# Подсчет частоты слов
from collections import Counter

words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
word_count = Counter(words)  # использует str как ключи

print(word_count)  # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(word_count['apple'])  # 3

Паттерн 3: Группировка по кортежу

# Анализ продаж по (месяц, товар)
sales = [
    {'month': 1, 'product': 'A', 'revenue': 1000},
    {'month': 1, 'product': 'B', 'revenue': 1500},
    {'month': 2, 'product': 'A', 'revenue': 1200},
    {'month': 2, 'product': 'B', 'revenue': 1800},
]

by_month_product = {}
for sale in sales:
    key = (sale['month'], sale['product'])
    by_month_product[key] = sale['revenue']

print(by_month_product[(1, 'A')])  # 1000
print(by_month_product[(2, 'B')])  # 1800

Паттерн 4: Кастомный класс как ключ

class User:
    def __init__(self, user_id, name):
        self.user_id = user_id
        self.name = name
    
    def __hash__(self):
        # Определяем как хешировать объект
        return hash(self.user_id)
    
    def __eq__(self, other):
        # Два пользователя равны если у них один ID
        return self.user_id == other.user_id

user1 = User(1, 'Alice')
user2 = User(1, 'Alice2')  # Тот же ID
user3 = User(2, 'Bob')

user_data = {user1: 'data1'}
user_data[user2] = 'data2'  # Перезапишет user1, т.к. они равны
user_data[user3] = 'data3'

print(len(user_data))  # 2 (user1 и user2 - это один ключ)

Производительность

import timeit

# Поиск по string ключу - O(1) в среднем
dict_str = {f'key_{i}': i for i in range(1000000)}
time_str = timeit.timeit(lambda: dict_str['key_500000'], number=1000000)

# Поиск по int ключу - еще быстрее
dict_int = {i: i for i in range(1000000)}
time_int = timeit.timeit(lambda: dict_int[500000], number=1000000)

print(f"String key: {time_str:.4f}s")
print(f"Int key: {time_int:.4f}s")
# Int ключи обычно быстрее

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

1. Hash collision

# Два разных объекта могут иметь один hash
# Но у них разные __eq__, поэтому они разные ключи
key1 = 'abc'
key2 = 'abc'
print(key1 == key2)      # True (одинаковые строки)
print(hash(key1) == hash(key2))  # True (одинаковый хеш)
# В словаре это один и тот же ключ

my_dict = {key1: 'first'}
my_dict[key2] = 'second'  # Перезапишет first

2. Порядок в словаре (Python 3.7+)

# В Python 3.7+ словарь сохраняет порядок ключей
my_dict = {'z': 1, 'a': 2, 'm': 3}

for key in my_dict:
    print(key)  # z, a, m (в порядке вставки)

3. Когда tuple можно использовать вместо list

# Если нужны hashable данные
data = [
    (1, 'Alice', 30),
    (2, 'Bob', 25),
    (3, 'Charlie', 35)
]

# Можешь создать индекс
by_id = {person[0]: person for person in data}
print(by_id[1])  # (1, 'Alice', 30)

# Или использовать tuple как ключ
by_name_age = {(person[1], person[2]): person[0] for person in data}
print(by_name_age[('Alice', 30)])  # 1

Итоговая таблица

ТипМожно ли ключомПримечание
str✅Самый частый
int✅Быстрый поиск
float✅Редко используется
tuple✅Полезна для составных ключей
bool✅На самом деле это int
None✅Для значений по умолчанию
frozenset✅Неизменяемое множество
list❌Изменяемый тип
dict❌Изменяемый тип
set❌Изменяемый тип
пользовательский класс✅Если определены hash и eq

Ключевой принцип: Ключ должен быть неизменяемым (immutable) и иметь стабильный хеш, который не меняется после создания объекта.