Ответы:
Вот все о Python, которые я смог собрать (возможно, больше, чем кто-либо хотел бы знать, но ответ был исчерпывающим).
dict
использует открытую адресацию для разрешения коллизий хешей (см. Ниже) (см. Dictobject.c: 296-297 ).O(1)
поиск по индексу).На рисунке ниже показано логическое представление хеш-таблицы 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>
). Но что, если этот слот занят !? Скорее всего, потому что другая запись имеет тот же хеш (коллизия хешей!)==
сравнение, а не is
сравнение) записи в слоте с хешем и ключом текущей записи, которую нужно вставить ( dictobject.c : 337 344-345 ) соответственно. Если оба совпадают, то он думает, что запись уже существует, сдается и переходит к следующей записи, которая будет вставлена. Если хеш или ключ не совпадают, начинается поиск .i+1, i+2, ...
и использовать первый доступный (это линейное зондирование). Но по причинам, красиво объясненным в комментариях (см. Dictobject.c: 33-126 ), CPython использует случайное зондирование . При случайном исследовании следующий слот выбирается в псевдослучайном порядке. Запись добавляется в первый пустой слот. Для этого обсуждения фактический алгоритм, используемый для выбора следующего слота, не очень важен (см. Dictobject.c: 33-126 для алгоритма исследования). Важно то, что слоты проверяются до тех пор, пока не будет найден первый пустой слот.dict
размер будет изменен, если он заполнен на две трети. Это позволяет избежать замедления поиска. (см. dictobject.h: 64-65 )ПРИМЕЧАНИЕ. Я провел исследование по реализации Python Dict в ответ на мой собственный вопрос о том, как несколько записей в dict могут иметь одинаковые значения хеш-функции. Я разместил здесь слегка отредактированную версию ответа, потому что все исследования очень актуальны и для этого вопроса.
Как реализованы встроенные словари Python?
Вот краткий курс:
Упорядоченный аспект неофициальный с Python 3.6 (чтобы дать другим реализациям возможность идти в ногу), но официальный в Python 3.7 .
Долгое время все работало именно так. Python будет предварительно выделять 8 пустых строк и использовать хеш, чтобы определить, куда нужно вставить пару ключ-значение. Например, если хеш для ключа закончился на 001, он поместил бы его в 1 (т. Е. 2-й) индекс (как в примере ниже.)
<hash> <key> <value>
null null null
...010001 ffeb678c 633241c4 # addresses of the keys and values
null null null
... ... ...
Каждая строка занимает 24 байта в 64-битной архитектуре, 12 - в 32-битной. (Обратите внимание, что заголовки столбцов - это просто метки для наших целей - на самом деле они не существуют в памяти.)
Если хеш завершился так же, как хеш существовавшего ранее ключа, это коллизия, и тогда она поместит пару ключ-значение в другое место.
После сохранения 5 значений ключа при добавлении другой пары ключ-значение вероятность коллизий хеша слишком велика, поэтому словарь увеличивается в два раза. В 64-битном процессе до изменения размера у нас осталось 72 байта, а после мы тратим 240 байтов из-за 10 пустых строк.
Это занимает много места, но время поиска довольно постоянное. Алгоритм сравнения ключей состоит в том, чтобы вычислить хеш, перейти в ожидаемое местоположение, сравнить идентификатор ключа - если это один и тот же объект, они равны. Если нет, то сравните значения хешей, если они не совпадают, они не равны. Иначе, мы наконец сравниваем ключи на равенство и, если они равны, возвращаем значение. Окончательное сравнение на равенство может быть довольно медленным, но более ранние проверки обычно сокращают окончательное сравнение, делая поиск очень быстрым.
Коллизии замедляют процесс, и злоумышленник теоретически может использовать хеш-коллизии для выполнения атаки типа «отказ в обслуживании», поэтому мы случайным образом инициализировали хеш-функцию так, чтобы она вычисляла разные хеш-функции для каждого нового процесса Python.
Описанное выше потраченное впустую пространство привело к изменению реализации словарей, добавив новую замечательную функцию, которая теперь упорядочивает словари путем вставки.
Вместо этого мы начинаем с предварительного выделения массива для индекса вставки.
Поскольку наша первая пара ключ-значение идет во второй слот, мы индексируем так:
[null, 0, null, null, null, null, null, null]
И наша таблица просто заполняется порядком вставки:
<hash> <key> <value>
...010001 ffeb678c 633241c4
... ... ...
Поэтому, когда мы ищем ключ, мы используем хеш, чтобы проверить ожидаемую позицию (в этом случае мы идем прямо к индексу 1 массива), а затем переходим к этому индексу в хеш-таблице (например, индекс 0 ), проверьте, чтобы ключи были равны (используя тот же алгоритм, описанный ранее), и, если это так, верните значение.
Мы сохраняем постоянное время поиска, с незначительными потерями скорости в одних случаях и выигрышами в других, с преимуществами, которые мы экономим довольно много места по сравнению с уже существующей реализацией, и мы сохраняем порядок вставки. Единственный потерянный пробел - нулевые байты в массиве индекса.
Раймонд Хеттингер (Raymond Hettinger) представил это на python-dev в декабре 2012 года. Наконец-то он попал в CPython в Python 3.6 . Упорядочение путем вставки считалось деталью реализации для 3.6, чтобы позволить другим реализациям Python шанс наверстать упущенное.
Другая оптимизация для экономии места - это реализация, которая разделяет ключи. Таким образом, вместо наличия избыточных словарей, которые занимают все это пространство, у нас есть словари, которые повторно используют общие ключи и хеши ключей. Вы можете думать об этом так:
hash key dict_0 dict_1 dict_2...
...010001 ffeb678c 633241c4 fffad420 ...
... ... ... ... ...
Для 64-битной машины это может сэкономить до 16 байт на ключ в каждом дополнительном словаре.
Эти общие ключи предназначены для использования в пользовательских объектах » __dict__
. Я полагаю, что для получения такого поведения необходимо завершить __dict__
заполнение своего объекта перед созданием следующего объекта ( см. PEP 412 ). Это означает, что вы должны назначить все свои атрибуты в __init__
или __new__
, иначе вы не сможете сэкономить место.
Однако, если вы знаете все свои атрибуты во время __init__
выполнения, вы также можете предоставить __slots__
свой объект и гарантировать, что он __dict__
вообще не создан (если он недоступен у родителей), или даже разрешить, __dict__
но гарантировать, что ваши предполагаемые атрибуты хранится в слотах в любом случае. Более подробную информацию о __slots__
, см мой ответ здесь .
**kwargs
в функции.find_empty_slot
: github.com/python/cpython/blob/master/Objects/dictobject.c # L969 - и начиная со строки 134, есть некоторая проза, которая описывает это.
Словари Python используют открытую адресацию ( ссылка внутри Beautiful code )
NB! Открытая адресация , иначе говоря, закрытое хеширование , не следует путать с противоположным открытым хэшированием!
Открытая адресация означает, что dict использует слоты массива, и когда в dict берется первичная позиция объекта, место объекта ищется по другому индексу в том же массиве, используя схему «возмущения», где значение хеш-функции объекта играет роль. ,