Как реализованы встроенные словари Python - PullRequest
239 голосов
/ 29 ноября 2008

Кто-нибудь знает, как реализован встроенный тип словаря для python? Насколько я понимаю, это какая-то хеш-таблица, но я не смог найти какого-либо однозначного ответа.

Ответы [ 3 ]

400 голосов
/ 26 января 2012

Вот все о Python, которые я смог собрать (возможно, больше, чем кто-либо хотел бы знать, но ответ был исчерпывающим).

  • Словари 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 ) соответственно. Если оба совпадают, то он думает, что запись уже существует, сдается и переходит к следующей записи, которая будет вставлена. Если хеш или ключ не совпадают, запускается probing .
  • Зондирование означает, что оно ищет слоты по слотам, чтобы найти пустые слоты. Технически мы могли бы просто пойти один за другим, i+1, i+2, ... и использовать первый доступный (это линейное зондирование). Но по причинам, красиво объясненным в комментариях (см. dictobject.c: 33-126 ), CPython использует случайное зондирование . При случайном исследовании следующий слот выбирается в псевдослучайном порядке. Запись добавляется в первый пустой слот. Для этого обсуждения фактический алгоритм, используемый для выбора следующего слота, не очень важен (см. dictobject.c: 33-126 для алгоритма зондирования). Важно то, что слоты проверяются до тех пор, пока не будет найден первый пустой слот.
  • То же самое происходит с поиском, только начинается с начального слота i (где i зависит от хэша ключа). Если хеш и ключ не совпадают с записью в слоте, он начинает зондирование, пока не найдет слот с соответствием. Если все слоты исчерпаны, выдается сообщение об ошибке.
  • Кстати, размер dict будет изменен, если он заполнен на две трети. Это позволяет избежать замедления поиска. (см. dictobject.h: 64-65 )

ПРИМЕЧАНИЕ. Я провел исследование по реализации Python Dict в ответ на мой собственный вопрос о том, как несколько записей в dict могут иметь одинаковые значения хеш-функции. Я разместил здесь слегка отредактированную версию ответа, потому что все исследования очень актуальны и для этого вопроса.

44 голосов
/ 08 июня 2010

Словари Python используют Открытая адресация ( ссылка внутри Beautiful code )

NB! Открытая адресация , также известная как закрытое хеширование не следует путать с противоположным открытым хешированием!

Открытая адресация означает, что в dict используются слоты массива, и когда первичная позиция объекта берется в dict, место объекта ищется по другому индексу в том же массиве, используя схему «возмущения», где хеш объекта значение играет роль.

35 голосов
/ 13 июня 2017

Как реализованы встроенные словари Python?

Вот краткий курс:

  • Это хеш-таблицы.
  • Новая процедура / алгоритм, начиная с Python 3.6, делает их
    • заказывается путем вставки ключа и
    • занимают меньше места,
    • практически без затрат на производительность.
  • Другая оптимизация экономит место, когда диктует общие ключи (в особых случаях).

Упорядоченный аспект неофициальный с Python 3.6, но официальный в Python 3.7 .

Словари Python - это хеш-таблицы

Долгое время это работало именно так. Python будет предварительно выделять 8 пустых строк и использовать хеш, чтобы определить, куда нужно вставить пару ключ-значение. Например, если хеш для ключа закончился на 001, он вставил бы его в индекс 1 (как в примере ниже.)

     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 . Упорядочение путем вставки по-прежнему считается деталью реализации, чтобы другие реализации 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__, см. Мой ответ здесь .

Смотри также:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...