Как реализован Python в словарях

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

+191
29 нояб. '08 в 7:35
источник поделиться
4 ответа

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

  • Словари 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 могут иметь одинаковые значения хэш-функции. Я опубликовал немного отредактированную версию ответа здесь, потому что все исследования очень актуальны и для этого вопроса.

+312
26 янв. '12 в 17:52
источник

Связанные вопросы


Похожие вопросы

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

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

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

+39
08 июн. '10 в 11:00
источник

Как создается Python в словарях?

Здесь короткий курс:

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

Предполагаемый аспект, вероятно, останется, но в настоящее время является неофициальным, и программы не должны записываться с этим ожиданием, как на Python 3.6.

Словари Python - это таблицы Hash

Долгое время он работал именно так. 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 пустых строк.

Это занимает много места, но время поиска довольно постоянное. Ключевым алгоритмом сравнения является вычисление хэша, переход к ожидаемому местоположению, сравнение ключевого идентификатора - если они являются одним и тем же объектом, они равны. Если нет, сравните хэш-значения, если они не совпадают, они не равны. Else, тогда мы, наконец, сравниваем ключи для равенства, а если они равны, верните значение. Окончательное сравнение для равенства может быть довольно медленным, но более ранние проверки обычно сокращают конечное сравнение, что делает поиск очень быстрым.

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

См. также:

+12
12 июн. '17 в 21:54
источник

Это хэш-таблица. Вы можете прочитать об этом в python wiki. В противном случае код хорошо написан и должен быть легко понят.

+2
29 нояб. '08 в 7:40
источник

Посмотрите другие вопросы по меткам или Задайте вопрос