Ответ 1
Здесь все о питонах Питона, которые я смог собрать (возможно, больше, чем кто-либо хотел бы знать, но ответ исчерпывающий).
- Словари Python реализованы как хэш-таблицы.
- Таблицы хэшей должны допускать хеш-коллизии, т.е. даже если два разных ключа имеют одинаковое значение хэша, реализация таблицы должна иметь стратегию для вставки и извлечения ключей и значений однозначно.
- Python
dict
использует открытую адресацию для разрешения хеш-коллизий (поясняется ниже) (см. dictobject.c: 296-297). - Хэш-таблица Python - это просто непрерывный блок памяти (вроде массива, поэтому вы можете выполнить поиск
O(1)
по индексу). - Каждый слот в таблице может хранить одну и только одну запись. Это важно.
- Каждая запись в таблице фактически представляет собой комбинацию из трех значений: < хэш, ключ, значение > . Это реализовано как структура C (см. dictobject.h: 51-56).
-
Рисунок ниже представляет собой логическое представление хэш-таблицы Python. На рисунке ниже
0, 1, ..., i, ...
слева находятся индексы слотов в хеш-таблице (они предназначены только для иллюстративных целей и не сохраняются вместе с таблицей, очевидно!).# Logical model of Python Hash table -+-----------------+ 0| <hash|key|value>| -+-----------------+ 1| ... | -+-----------------+ .| ... | -+-----------------+ i| ... | -+-----------------+ .| ... | -+-----------------+ n| ... | -+-----------------+
-
Когда инициализируется новый dict, он начинается с 8 слотов. (см. dictobject.h: 49)
- При добавлении записей в таблицу, мы начинаем с некоторого слота
i
, который основан на хэше ключа. CPython изначально используетi = hash(key) & mask
(гдеmask = PyDictMINSIZE - 1
, но это не очень важно). Просто отметьте, что начальный слотi
, который установлен, зависит от хэша ключа. - Если этот слот пуст, запись добавляется в слот (путем ввода, я имею в виду
<hash|key|value>
). Но что, если этот слот занят!? Скорее всего, потому, что другая запись имеет тот же хеш (хеш-коллизия!) - Если слот занят, CPython (и даже PyPy) сравнивает хэш И ключ (по сравнению я имею в виду
==
сравнение не сравнениеis
) записи в слоте против хеша и ключа текущей записи, которая будет вставлена (dictobject.c: 337,344-345) соответственно. Если оба совпадают, то он считает, что запись уже существует, отбрасывается и переходит к следующей записи, которую нужно вставить. Если хэш или ключ не совпадают, он запускает зондирование. - Проверка просто означает, что он ищет слоты по слоту, чтобы найти пустой слот. Технически мы могли бы просто пойти один за другим,
i+1, i+2, ...
и использовать первый доступный (это линейное исследование). Но по причинам, объясняемым красиво в комментариях (см. dictobject.c: 33-126), CPython использует случайное зондирование. При случайном зондировании следующий слот выбирается в псевдослучайном порядке. Запись добавляется в первый пустой слот. Для этого обсуждения фактический алгоритм, используемый для выбора следующего слота, не очень важен (см. dictobject.c: 33-126 для алгоритма для зондирования). Важно то, что слоты исследуются до тех пор, пока не будет найден первый пустой слот. - То же самое происходит и для поисков, просто начинается с начального слота я (где я зависит от хэша ключа). Если хэш и ключ не совпадают с входом в слот, он начинает зондирование, пока не найдет слот с совпадением. Если все слоты исчерпаны, он сообщает об ошибке.
- BTW, размер
dict
будет изменен, если он заполнен на две трети. Это позволяет избежать замедления поиска. (см. dictobject.h: 64-65)
ПРИМЕЧАНИЕ. Я провел исследование по реализации Python Dict в ответ на мой собственный question о том, как несколько записей в dict могут иметь одинаковые значения хэш-функции. Я опубликовал немного отредактированную версию ответа здесь, потому что все исследования очень актуальны и для этого вопроса.