Почему порядок в словарях и наборах произволен?

Я не понимаю, как цикл по словарю или набор в python выполняется "произвольным" порядком.

Я имею в виду, это язык программирования, поэтому все на языке должно быть на 100% определено, правильно? Python должен иметь какой-то алгоритм, который определяет, какая часть словаря или набора выбрана, первая, вторая и т.д.

Что мне не хватает?

Ответ 1

Порядок не является произвольным, но зависит от истории вставки и удаления словаря или набора, а также от конкретной реализации Python. В оставшейся части этого ответа, для словаря, вы также можете прочитать "set"; наборы реализованы в виде словарей с просто ключами и без значений.

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

Перечисление содержимого проходит по слотам, поэтому ключи перечислены в том порядке, в котором они в данный момент находятся в таблице.

Возьмем, к примеру, ключи 'foo' и 'bar', и предположим, что размер таблицы составляет 8 слотов. В Python 2.7 hash('foo') равен -4177197833195190597, hash('bar') равен 327024216814240868. По модулю 8 это означает, что эти два ключа расположены в слотах 3 и 4, а затем:

>>> hash('foo')
-4177197833195190597
>>> hash('foo') % 8
3
>>> hash('bar')
327024216814240868
>>> hash('bar') % 8
4

Это информирует их порядок перечисления:

>>> {'bar': None, 'foo': None}
{'foo': None, 'bar': None}

Все слоты, кроме 3 и 4, пусты, и в цикле по таблице сначала перечисляются слоты 3, а затем слоты 4, поэтому перед 'bar' отображается 'foo' 'bar'.

bar и baz имеют значения хеш-функции, которые разделены ровно на 8 и, следовательно, отображаются в один и тот же слот 4:

>>> hash('bar')
327024216814240868
>>> hash('baz')
327024216814240876
>>> hash('bar') % 8
4
>>> hash('baz') % 8
4

Их порядок теперь зависит от того, какой ключ был выделен первым; второй ключ должен быть перемещен в следующий слот:

>>> {'baz': None, 'bar': None}
{'bar': None, 'baz': None}
>>> {'bar': None, 'baz': None}
{'baz': None, 'bar': None}

Порядок таблиц здесь отличается, потому что один или другой ключ был выделен первым.

Техническое имя для базовой структуры, используемой CPython (наиболее часто используемая реализация Python), - это хеш-таблица, в которой используется открытая адресация. Если вы любопытны и достаточно хорошо понимаете C, взгляните на реализацию C для всех (хорошо задокументированных) деталей. Вы также можете посмотреть эту презентацию Pycon 2010 Брэндона Роудса о том, как работает CPython dict, или взять копию Beautiful Code, которая включает главу о реализации, написанную Эндрю Кучлингом.

Обратите внимание, что в Python 3.3 также используется случайное начальное число хешей, что делает коллизии хеша непредсказуемыми, чтобы предотвратить определенные типы отказа в обслуживании (когда злоумышленник делает сервер Python без ответа, вызывая массовые коллизии хеша). Это означает, что порядок данного словаря также зависит от случайного начального числа хэша для текущего вызова Python.

Другие реализации могут свободно использовать другую структуру для словарей, если они удовлетворяют документированному интерфейсу Python для них, но я считаю, что все реализации до сих пор используют разновидность хеш-таблицы.

CPython 3.6 представляет новую реализацию dict которая поддерживает порядок вставки, а также быстрее и эффективнее при загрузке. Вместо того чтобы хранить большую разреженную таблицу, в которой каждая строка ссылается на сохраненное хеш-значение, а также на объекты ключа и значения, новая реализация добавляет меньший хеш-массив, который ссылается только на индексы в плотной таблице (та, которая содержит столько строк, сколько существует фактических пары ключ-значение), и это плотная таблица, в которой перечисляются содержащиеся элементы по порядку. Смотрите предложение к Python-Dev для более подробной информации. Обратите внимание, что в Python 3.6 это считается деталью реализации, Python-the-language не указывает, что другие реализации должны сохранять порядок. Это изменилось в Python 3.7, где эта деталь была повышена до языковой спецификации; для того чтобы любая реализация была должным образом совместима с Python 3.7 или новее, она должна скопировать это поведение, сохраняющее порядок.

Python 2.7 и новее также предоставляет класс OrderedDict, подкласс dict который добавляет дополнительную структуру данных для записи порядка ключей. Ценой некоторой скорости и дополнительной памяти этот класс запоминает, в каком порядке вы вставляли ключи; перечисление ключей, значений или элементов будет происходить в таком порядке. Он использует двусвязный список, хранящийся в дополнительном словаре, чтобы эффективно поддерживать порядок в актуальном состоянии. Посмотрите пост Раймонда Хеттингера с изложением этой идеи. Обратите внимание, что тип set все еще неупорядочен.

Если вам нужен заказанный набор, вы можете установить пакет oset; это работает на Python 2.5 и выше.

Ответ 2

Это скорее ответ на Python 3.41 Набор, прежде чем он будет закрыт как дубликат.


Другие правы: не полагайтесь на заказ. Даже не притворяйтесь, что есть один.

Тем не менее, есть одна вещь, на которую можно положиться:

list(myset) == list(myset)

То есть порядок стабилен.


Понимание того, почему существует воспринимаемый порядок, требует понимания нескольких вещей:

  • Этот Python использует хэш-множества,

  • Как набор хешей CPython хранится в памяти и

  • Как числа хэшируются

Сверху:

Хэш-набор - это метод хранения случайных данных с очень быстрым временем поиска.

Он имеет массив поддержки:

# A C array; items may be NULL,
# a pointer to an object, or a
# special dummy object
_ _ 4 _ _ 2 _ _ 6

Мы проигнорируем специальный фиктивный объект, который существует только для того, чтобы упростить удаление, потому что мы не будем удалять из этих множеств.

Чтобы иметь быстрый поиск, вы делаете магию для вычисления хэша с объекта. Единственное правило состоит в том, что два одинаковых объекта имеют один и тот же хеш. (Но если два объекта имеют один и тот же хеш, они могут быть неравными.)

Затем вы создаете индекс, беря модуль по длине массива:

hash(4) % len(storage) = index 2

Это позволяет быстро получить доступ к элементам.

Хеши - это только большая часть истории, так как hash(n) % len(storage) и hash(m) % len(storage) могут привести к тому же числу. В этом случае несколько разных стратегий могут попытаться разрешить конфликт. CPython использует "линейное исследование" 9 раз, прежде чем делать сложные вещи, поэтому он будет смотреть слева от слота до 9 мест, прежде чем искать в другом месте.

Наборы хешей CPython хранятся следующим образом:

  • Хэш-набор может быть не более 2/3 полным. Если имеется 20 элементов, а базовый массив - 30 элементов, хранилище резервных копий будет иметь размер больше. Это связано с тем, что вы чаще сталкиваетесь с столкновениями с небольшими резервными хранилищами, а столкновения замедляют все.

  • Резервное хранилище изменяет размеры в 4, начиная с 8, за исключением больших наборов (50 тыс. элементов), которые изменяют размер в два раза: (8, 32, 128,...).

Поэтому, когда вы создаете массив, резервное хранилище имеет длину 8. Когда он будет заполнен 5 и вы добавите элемент, он вкратце будет содержать 6 элементов. 6 > ²⁄₃·8, так что это приведет к изменению размера, а хранилище резервных копий - к размеру 32.

Наконец, hash(n) просто возвращает n для чисел (кроме -1, который является специальным).


Итак, посмотрим на первый:

v_set = {88,11,1,33,21,3,7,55,37,8}

len(v_set) равно 10, поэтому хранилище поддержки составляет не менее 15 (+1) после добавления всех элементов. Соответствующая мощность 2 равна 32. Таким образом, резервное хранилище:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

Мы имеем

hash(88) % 32 = 24
hash(11) % 32 = 11
hash(1)  % 32 = 1
hash(33) % 32 = 1
hash(21) % 32 = 21
hash(3)  % 32 = 3
hash(7)  % 32 = 7
hash(55) % 32 = 23
hash(37) % 32 = 5
hash(8)  % 32 = 8

чтобы они вставлялись как:

__  1 __  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __
   33 ← Can't also be where 1 is;
        either 1 or 33 has to move

Итак, мы ожидаем такой порядок, как

{[1 or 33], 3, 37, 7, 8, 11, 21, 55, 88}

с 1 или 33, который не находится в начале в другом месте. Это будет использовать линейное зондирование, поэтому мы либо будем иметь:

       ↓
__  1 33  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

или

       ↓
__ 33  1  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __

Вы можете ожидать, что 33 будет тем, кто был перемещен, потому что 1 уже был там, но из-за изменения размера, которое происходит по мере создания набора, на самом деле это не так. Каждый раз, когда набор становится перестроенным, элементы, которые уже добавлены, эффективно переупорядочиваются.

Теперь вы можете понять, почему

{7,5,11,1,4,13,55,12,2,3,6,20,9,10}

может быть в порядке. Есть 14 элементов, поэтому хранилище поддержки составляет не менее 21 + 1, что означает 32:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __

От 1 до 13 хэшей в первых 13 слотах. 20 идет в слот 20.

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ __ __ __ __ __ __ __ __ __

55 идет в слот hash(55) % 32, который равен 23:

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ 55 __ __ __ __ __ __ __ __

Если бы мы выбрали 50 вместо этого, мы ожидали бы

__  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ 50 __ 20 __ __ __ __ __ __ __ __ __ __ __

И вот и вот:

{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 20, 50}
#>>> {1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 50, 20}

pop реализуется довольно просто взглядом вещей: он перемещается по списку и всплывает первый.


Это все детали реализации.

Ответ 3

"Произвольное" - это не то же самое, что "undefined".

То, что они говорят, заключается в том, что нет никаких полезных свойств итерационного порядка словаря, которые являются "в публичном интерфейсе". Почти наверняка есть много свойств итерационного порядка, которые полностью определяются кодом, который в настоящее время реализует итерацию словаря, но авторы не обещают их вам как что-то, что вы можете использовать. Это дает им больше свободы для изменения этих свойств между версиями Python (или даже просто в разных условиях работы или полностью случайным образом во время выполнения), не беспокоясь о том, что ваша программа сломается.

Таким образом, если вы пишете программу, которая зависит от любого свойства во всех словарных порядках, то вы "нарушаете контракт" с использованием типа словаря, а разработчики Python не обещают, что это всегда будет работать, даже если это похоже, работает сейчас, когда вы его проверяете. Это в основном эквивалент использования "undefined поведения" в C.

Ответ 4

Другие ответы на этот вопрос превосходны и хорошо написаны. ОП спрашивает "как", который я интерпретирую как "как им избежать" или "почему".

Документация Python гласит, что dictionaries не упорядочены, потому что словарь Python реализует абстрактный тип данных ассоциативный массив. Как говорится

порядок возврата привязок может быть произвольным

Другими словами, студент-информатика не может предположить, что ассоциативный массив упорядочен. То же самое верно для наборов в math

порядок, в котором перечислены элементы набора, не имеет значения

и информатика

набор представляет собой абстрактный тип данных, который может хранить определенные значения без какого-либо определенного порядка

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

Ответ 5

Python использует хеш-таблицу для хранения словарей, поэтому в словарях или других итерабельных объектах нет порядка, используйте хеш-таблицу.

Но в отношении индексов элементов хэш-объекта python вычисляет индексы на основе следующего кода внутри hashtable.c:

key_hash = ht->hash_func(key);
index = key_hash & (ht->num_buckets - 1);

Поэтому, поскольку хеш-значение целых чисел является самим целым числом * индекс основан на числе (ht->num_buckets - 1 является константой), поэтому индекс, вычисленный поразмерности и между (ht->num_buckets - 1) и само число * (ожидание для -1, которое оно hash равно -2), и для других объектов с их хэш-значением.

рассмотрим следующий пример с set, который использует хеш-таблицу:

>>> set([0,1919,2000,3,45,33,333,5])
set([0, 33, 3, 5, 45, 333, 2000, 1919])

Для числа 33 имеем:

33 & (ht->num_buckets - 1) = 1

На самом деле это:

'0b100001' & '0b111'= '0b1' # 1 the index of 33

Обратите внимание, что в этом случае (ht->num_buckets - 1) есть 8-1=7 или 0b111.

И для 1919:

'0b11101111111' & '0b111' = '0b111' # 7 the index of 1919

И для 333:

'0b101001101' & '0b111' = '0b101' # 5 the index of 333

Для получения более подробной информации о хэш-функции python полезно прочитать следующие цитаты из исходный код python:

Основные тонкости впереди: большинство схем хеширования зависят от наличия "хорошего" хэша функции, в смысле моделирования случайности. Python не делает: важные хеш-функции (для строк и ints) очень регулярны случаи:

>>> map(hash, (0, 1, 2, 3))
  [0, 1, 2, 3]
>>> map(hash, ("namea", "nameb", "namec", "named"))
  [-1658398457, -1658398460, -1658398459, -1658398462]

Это не обязательно плохо! Напротив, в таблице размера 2 ** i, принимая младшие биты i, поскольку индекс начальной таблицы чрезвычайно быстрый, и там вообще не являются столкновениями для индексов, проиндексированных непрерывным диапазоном int. То же самое примерно верно, когда клавиши являются "последовательными" строками. Итак, это дает лучшее, чем случайное поведение в общих случаях, и это очень желательно.

OTOH, когда происходят столкновения, тенденция заполнять смежные срезы хеш-таблица делает хорошую стратегию разрешения конфликтов решающей. Принимая только последние я биты хэш-кода также уязвимы: например, рассмотрите список [i << 16 for i in range(20000)] как набор ключей. Поскольку ints являются их собственными хеш-кодами, и это соответствует типу размера 2 ** 15, последние 15 бит каждого хэш-кода - все 0: все они сопоставляются с одним и тем же индексом таблицы.

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


<суб > * Хэш-функция для класса int:

class int:
    def __hash__(self):
        value = self
        if value == -1:
            value = -2
        return value

суб >