1. Хеш-таблицы: основа словарей
Техническая реализация словарей (dict) в Python: как это работает под капотом
Словари (dict) — одна из самых оптимизированных структур данных в Python. Их скорость и гибкость достигаются за счет продуманной внутренней реализации на основе хеш-таблиц. В этой статье мы разберем, как устроены словари в CPython (стандартной реализации Python), как они хранят данные, обрабатывают коллизии и обеспечивают константное время доступа O(1) в среднем случае.
1. Хеш-таблицы: основа словарей
Словарь в Python — это хеш-таблица, которая состоит из массива “ведер” (buckets). Каждое ведро хранит:
- Хеш ключа (hash),
- Ключ (key),
- Значение (value).
При добавлении элемента:
- Вычисляется хеш ключа через встроенную функцию
hash(). - На основе хеша определяется индекс ведра в массиве.
- Если ведро пустое — данные сохраняются в него.
- Если ведро занято — возникает коллизия, и система ищет следующее свободное ведро по определенному алгоритму.
2. Разрешение коллизий: метод открытой адресации
В Python для разрешения коллизий используется открытая адресация (open addressing), а не связные списки (как в некоторых других языках).
Алгоритм поиска свободного ведра:
- Вычисляется стартовый индекс:
index = hash(key) % table_size. - Если ведро занято, проверяется следующее по формуле:
new_index = (5 * index + 1 + perturb) % table_size,
гдеperturb— вспомогательная переменная для псевдослучайного смещения (чтобы избежать кластеризации).
Пример поиска места для ключа "name":
hash("name") = 123456789
index = 123456789 % 8 = 5 # Предположим, размер таблицы 8
# Если ведро 5 занято, проверяем 5 + 1, затем 5 + 6 и т.д.
3. Структура хеш-таблицы в CPython
Начиная с Python 3.6, реализация словарей была оптимизирована для экономии памяти. Теперь данные хранятся в двух массивах:
- Индексный массив (indices):
Содержит индексы (номера) записей в основном массиве.
Размер: степень двойки (например, 8, 16, 32). - Основной массив (entries):
Хранит структурыPyDictKeyEntry, включающие хеш, ключ и значение.
Порядок элементов соответствует порядку их добавления (поэтому словари сохраняют порядок в Python 3.7+).
Пример структуры:
struct PyDictKeyEntry {
Py_hash_t hash; // Хеш ключа
PyObject* key; // Указатель на ключ
PyObject* value; // Указатель на значение
};
4. Процесс вставки элемента
Рассмотрим пример вставки пары "name": "Anna":
- Вычисляется хеш ключа:
hash("name"). - Определяется индекс в индексном массиве:
hash % indices_size. - Если индекс свободен:
- В основной массив добавляется новая запись.
- В индексном массиве сохраняется индекс этой записи.
- Если индекс занят (коллизия):
- Поиск следующего свободного места в индексном массиве.
- Обновление основной записи или создание новой.
5. Удаление элемента
При удалении элемента:
- Находится соответствующая запись в основном массиве.
- Значение помечается как удаленное (dummy), чтобы не нарушать цепочки поиска при коллизиях.
- При последующих вставках “удаленные” ведра могут быть переиспользованы.
6. Изменение размера таблицы
Хеш-таблица динамически расширяется при заполнении.
Правило: если таблица заполнена на 2/3, она увеличивается в 2–4 раза.
При этом:
- Создается новый индексный массив большего размера.
- Все существующие элементы перехэшируются и перераспределяются.
Пример:
- Исходный размер: 8 ведер.
- При добавлении 6-го элемента (6/8 = 0.75 ≥ 2/3) размер увеличивается до 16.
7. Защита от атак через коллизии
В Python (с версии 3.3+) используется рандомизация хешей (Hash randomization):
- При запуске интерпретатора генерируется случайное число (
PYTHONHASHSEED), которое добавляется к хешам строк и некоторых других объектов. - Это предотвращает атаки, когда злоумышленник создает множество ключей с одинаковыми хешами, чтобы замедлить работу словаря.
8. Пример: как работает поиск элемента
Рассмотрим словарь d = {"a": 1, "b": 2}:
- Хеш
"a"= 1245,"b"= 5678. - Размер индексного массива = 8.
- Индексы:
"a"→ 1245 % 8 = 5"b"→ 5678 % 8 = 2
- Основной массив хранит записи в порядке добавления:
entries = [ (hash("a"), "a", 1), (hash("b"), "b", 2) ] - Индексный массив:
indices = [None, None, 1, None, None, 0, None, None] # indices[5] → 0 (первая запись), indices[2] → 1 (вторая запись)
9. Оптимизации в CPython
- Compact Dictionaries (Python 3.6+):
Основной массив хранит записи в порядке добавления, что позволяет быстро итерировать по словарю. - Shared Keys (Python 3.3+):
Словари с одинаковым набором ключей могут разделять метаданные, экономя память. - Caching Хешей:
Для часто используемых ключей (например, строк) хеши кэшируются, чтобы избежать повторных вычислений.
10. Производительность: когда словарь замедляется
- Высокий коэффициент заполнения (близкий к 2/3): увеличивается количество коллизий.
- Большое количество удалений: множество “удаленных” меток замедляет поиск.
- Слабые хеш-функции: если хеши ключей плохо распределены, коллизии учащаются.
11. Как посмотреть внутреннюю структуру словаря
Стандартный Python не предоставляет API для доступа к внутренним массивам словаря, но можно использовать модуль gc или C-расширения. Пример через gc (для образовательных целей):
import gc
d = {"a": 1, "b": 2}
# Получаем список объектов-словарей
for obj in gc.get_objects():
if isinstance(obj, dict) and obj == d:
print("Dict internals:", obj.__dict__)
Заключение
Словари в Python — это результат многолетней оптимизации. Их реализация на основе хеш-таблиц с открытой адресацией, компактным хранением и защитой от атак делает их быстрыми и надежными. Понимание внутренней работы помогает:
- Избегать патологических сценариев (например, множества коллизий).
- Осознанно выбирать ключи (использовать хешируемые и неизменяемые типы).
- Оптимизировать код, работающий с большими объемами данных.