Как реализованы встроенные словари Python?
Кто-нибудь знает, как реализован встроенный тип словаря для python? Насколько я понимаю, это своего рода хэш-таблица, но я не смог найти какого-либо окончательного ответа.
Переведено автоматически
Ответ 1
Редактировать:
Этот ответ предназначен для версий Python более ранних, чем 3.6. Для Python 3.6 и далее смотрите Ответ России-необходимо-удалить-Путина ниже.
Оригинал:
Вот все о Python dicts, которые я смог собрать воедино (вероятно, больше, чем кто-либо хотел бы знать; но ответ исчерпывающий).
Словари 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
comparison) записи в слоте с хэшем и ключом текущей вставляемой записи (dictobject.c:337,344-345) соответственно. Если оба совпадают, то он думает, что запись уже существует, сдается и переходит к следующей вставляемой записи. Если хэш или ключ не совпадают, начинается проверка.Зондирование просто означает поиск слотов за слотом, чтобы найти пустой слот. Технически мы могли бы просто перейти по одному,
i+1, i+2, ...
и использовать первый доступный (это линейное зондирование). Но по причинам, прекрасно объясненным в комментариях (см. dictobject.c:33-126), CPython использует случайное зондирование. При случайном поиске следующий слот выбирается в псевдослучайном порядке. Запись добавляется в первый пустой слот. Для этого обсуждения фактический алгоритм, используемый для выбора следующего слота, на самом деле не важен (см. dictobject.c: 33-126 Алгоритм поиска). Важно то, что слоты исследуются до тех пор, пока не будет найден первый пустой слот.То же самое происходит и при поиске, просто начинается с начального слота i (где i зависит от хэша ключа). Если хэш и ключ не совпадают с записью в слоте, он начинает поиск, пока не найдет слот с совпадением. Если все слоты исчерпаны, он сообщает о сбое.
Кстати, размер
dict
будет изменен, если он заполнен на две трети. Это позволяет избежать замедления поиска. (см. dictobject.h: 64-65)
ПРИМЕЧАНИЕ: Я провел исследование реализации Python Dict в ответ на мой собственный вопрос о том, как несколько записей в dict могут иметь одинаковые хэш-значения. Я опубликовал здесь слегка отредактированную версию ответа, потому что все исследования очень актуальны и для этого вопроса.
Ответ 2
Как реализованы встроенные словари Python?
Вот краткий курс:
- Это хэш-таблицы. (Смотрите ниже о специфике реализации Python.)
- Новая компоновка и алгоритм, начиная с Python 3.6, делают их
- упорядочены по вставке ключа и
- занимают меньше места,
- практически без снижения производительности.
- Другая оптимизация экономит место, когда dicts совместно используют ключи (в особых случаях).
Упорядоченный аспект является неофициальным начиная с Python 3.6 (чтобы дать шанс другим реализациям не отставать), но официальным в Python 3.7.
Словари Python представляют собой хэш-таблицы
Долгое время это работало именно так. 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), проверяем, что ключи равны (используя тот же алгоритм, описанный ранее), и, если это так, возвращаем значение.
Мы сохраняем постоянное время поиска, с незначительными потерями скорости в одних случаях и выигрышем в других, с плюсами в том, что мы экономим довольно много места по сравнению с ранее существующей реализацией и сохраняем порядок вставки. Единственное потраченное впустую пространство - это нулевые байты в индексном массиве.
Рэймонд Хеттингер представил это на 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__
or __new__
, иначе вы можете не получить экономии места.
Однако, если вы знаете все свои атрибуты во время выполнения вашего __init__
, вы также можете предоставить __slots__
для своего объекта и гарантировать, что __dict__
он вообще не создается (если недоступен в родительских файлах), или даже разрешить, __dict__
но гарантировать, что ваши предусмотренные атрибуты в любом случае хранятся в слотах. Подробнее о __slots__
, смотрите мой ответ здесь.
Смотрите также:
- PEP 509 -- Добавление частной версии в dict
- PEP 468 -- Сохранение порядка
**kwargs
в функции. - PEP 520 -- Сохранение порядка определения атрибутов класса
- PyCon 2010: словарь возможностей - Брэндон Роудс
- PyCon 2017: словарь стал еще мощнее - Брэндон Роудс
- PyCon 2017: современные словари Python - слияние дюжины отличных идей - Раймонд Хеттингер
- dictobject.c - фактическая реализация dict CPython на C.
Ответ 3
Словари Python используют открытую адресацию (ссылка внутри красивого кода)
Открытая адресация ПРИМЕЧАНИЕ!, также известную как закрытое хеширование, не следует, как отмечено в Википедии, путать с противоположным ей открытым хешированием!
Открытая адресация означает, что dict использует слоты массива, и когда в dict выбирается первичная позиция объекта, место объекта ищется по другому индексу в том же массиве, используя схему "возмущения", где значение хэша объекта играет роль.
Ответ 4
Python dict теперь поддерживает два индекса. Один представляет собой разреженный массив. Это то, что он делает, когда первый элемент вставляется в dict:
- вставить значение ключа
- dict находит хэш ключа
- dict сопоставляет хэш с индексом
- в разреженном массиве находится этот индекс и вводится нулевое число (при первом вводе)
Второй массив - это плотный массив. Вот что там происходит: - В нулевом индексе введите значение
Таким образом, второй массив компактен и экономит память.
Для последующих вставок увеличивается индекс вставки второго массива. Таким образом экономится память и сохраняется порядок вставки.
При вставке индекса в первый разреженный массив могут возникать хэш-коллизии. Об этом заботится псевдослучайный поиск, т. Е. алгоритм, который просматривает пустые ячейки внутри массива предсказуемым, но псевдослучайным способом.
Второй массив всегда компактный.