Відповіді:
Тут є все про дикти 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| ... |
-+-----------------+
Коли ініціалізується новий дікт, він починається з 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 у відповідь на власне запитання про те, як кілька записів у дикті можуть мати однакові хеш-значення. Тут я розмістив трохи відредаговану версію відповіді, оскільки всі дослідження дуже актуальні і для цього питання.
Як реалізуються вбудовані словники 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 ), переконайтеся, що ключі рівні (використовуючи той же алгоритм, описаний раніше), і якщо так, поверніть значення.
Ми зберігаємо постійний час пошуку, з невеликими втратами швидкості в одних випадках і збільшенням в інших, з перевагами, що ми економимо досить багато місця перед попередньою реалізацією і зберігаємо порядок вставки. Єдине пропущене місце - це нульові байти в масиві індексів.
Реймонд Хеттінгер представив це на 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 використовують відкриту адресацію ( посилання на прекрасний код )
NB! Відкрита адресація , також закрита хешшю , як зазначається у Вікіпедії, не повинна плутати з її протилежним відкритим хешированием!
Відкрита адресація означає, що дикт використовує слоти масиву, і коли основне положення об'єкта займається в дикті, місце об'єкта шукається за іншим індексом у тому ж масиві, використовуючи схему "збурення", де грає значення хеш-об'єкта об'єкта .