Подтвердить что ты не робот

Почему ключ словаря с плавающей запятой перезаписывает целочисленный ключ с тем же значением?

Я работаю через http://www.mypythonquiz.com, а вопрос № 45 запрашивает вывод следующий код:

confusion = {}
confusion[1] = 1
confusion['1'] = 2
confusion[1.0] = 4

sum = 0
for k in confusion:
    sum += confusion[k]

print sum

Выходной сигнал 6, так как клавиша 1.0 заменяет 1. Это немного опасно для меня, это всегда полезная функция языка?

4b9b3361

Ответ 1

Вы должны учитывать, что dict имеет целью хранить данные в зависимости от логического числового значения, а не от того, как вы его представляли.

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

Таким образом, при использовании разных типов по техническим причинам Python пытается скрыть данные реализации, а преобразование intfloat выполняется автоматически.

Было бы гораздо более удивительно, если бы в программе Python if x == 1: ... не было принято, когда x является float со значением 1.

Обратите внимание, что также с Python 3 значение 1/2 равно 0.5 (деление двух целых чисел) и что типы long и строка, отличная от unicode, были удалены с той же попыткой скрыть детали реализации.

Ответ 2

Прежде всего: поведение явно описано в документах для функции hash:

hash(object)

Возвращает хэш-значение объекта (если оно есть). Значения хэша целые числа. Они используются для быстрого сравнения ключей словаря во время поиск словаря. Числовые значения, сравнивающие одинаковые, имеют одинаковые хэш-значение (даже если они имеют разные типы, как в случае 1и 1.0).

Во-вторых, ограничение хэширования указывается в документах для object.__hash__

object.__hash__(self)

Вызывается встроенной функцией hash() и для операций с членами хешированные коллекции, включая set, frozenset и dict. __hash__()должен возвращать целое число. Единственное требуемое свойство - объекты которые сравнивают одинаковые, имеют одно и то же значение хэша;

Это не уникально для python. Java имеет одинаковую оговорку: если вы реализуете hashCode, то для правильной работы вещей вы должны реализовывать его таким образом, чтобы: x.equals(y) подразумевал x.hashCode() == y.hashCode().

Итак, python решил, что 1.0 == 1 выполняется, поэтому он вынужден предоставить реализацию для hash такой, что hash(1.0) == hash(1). Побочным эффектом является то, что 1.0 и 1 действуют точно так же, как клавиши dict, следовательно, поведение.

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

Если бы мы имели 1.0 == 1, но hash(1.0) != hash(1), мы все равно могли бы столкнуться. И если 1.0 и 1 сталкиваются, dict будет использовать равенство, чтобы быть уверенным, являются ли они одним и тем же ключом или нет, а kaboom значение перезаписывается, даже если вы предполагали, что они будут разными.

Единственный способ избежать этого - иметь 1.0 != 1, чтобы dict мог различать их даже в случае столкновения. Но было более важно иметь 1.0 == 1, чем избегать поведения, которое вы видите, поскольку вы практически никогда не используете float и int как словарные ключи.

Так как python пытается скрыть различие между числами, автоматически преобразуя их при необходимости (например, 1/2 -> 0.5), имеет смысл, что это поведение отражается даже в таких обстоятельствах. Это больше согласуется с остальной частью python.


Такое поведение появляется в любой реализации, где совпадение ключей по крайней мере частично (как в хэш-карте) на основе сравнений.

Например, если dict был реализован с использованием красно-черного дерева или другого сбалансированного BST, при поиске ключевого слова 1.0 сравнения с другими ключами будут возвращать те же результаты, что и для 1 и поэтому они все равно будут действовать одинаково.

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


Обратите внимание, что есть способ исправить это: иметь отдельную хэш-карту/BST для каждого типа, вставленного в словарь. Таким образом, не могло быть никаких столкновений между объектами различного типа и как сравнение == не имеет значения, когда аргументы имеют разные типы.

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

Если вы использовали BST, вам сначала нужно найти тип и выполнить второй поиск. Поэтому, если вы собираетесь использовать много типов, вы в итоге получите вдвое больше работы (и поиск будет принимать O (log n) вместо O (1)).

Ответ 3

В python:

1==1.0
True

Это связано с неявным литьем

Однако:

1 is 1.0
False

Я понимаю, почему автоматическое кастинг между float и int является удобным, относительно безопасно лить int в float, и все же есть другие языки (например, go), которые держатся подальше от неявного литья.

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

Ответ 4

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

Если у вас есть два ключевых значения, сравнивающих одинаковые, но имеющие разные хэши, вы можете получить противоречивые результаты в зависимости от того, находилось ли другое значение ключа в поиске или нет. Например, это будет более вероятно, когда таблица будет заполнена. Этого можно избежать. Похоже, что разработчики Python имели это в виду, поскольку встроенная функция hash возвращает тот же хеш для эквивалентных числовых значений, независимо от того, являются ли эти значения int или float. Обратите внимание, что это распространяется на другие числовые типы, False равно 0 и True равно 1. Даже fractions.Fraction и decimal.Decimal поддерживают это свойство.

Требование, что если a == b then hash(a) == hash(b) задокументировано в определении object.__hash__():

Вызывается встроенной функцией hash() и для операций с элементами хешированных коллекций, включая set, frozenset и dict. __hash__() должен возвращать целое число. Единственное требуемое свойство состоит в том, что объекты, которые сравнивают одинаковые, имеют одно и то же значение хэш-функции; рекомендуется каким-то образом смешивать (например, с использованием эксклюзивных или) хеш-значений для компонентов объекта, которые также играют роль в сравнении объектов.

TL; DR: словарь сломается, если ключи, сравнимые с равными, не сопоставляются с одним и тем же значением.

Ответ 5

Честно говоря, противоположное опасно! 1 == 1.0, поэтому маловероятно представить, что если бы вы указали на разные ключи и попытались получить к ним доступ на основе оцененного числа, тогда вы, вероятно, столкнетесь с проблемой, потому что двусмысленность трудно понять.

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

Ответ 6

Я согласен с другими, что имеет смысл рассматривать 1 и 1.0 как то же самое в этом контексте. Даже если бы Python относился к ним по-другому, было бы неплохо попытаться использовать 1 и 1.0 как разные ключи для словаря. С другой стороны, мне трудно понять естественный случай использования 1.0 как псевдоним для 1 в контексте ключей. Проблема в том, что либо ключ является буквальным, либо он вычисляется. Если это буквальный ключ, то почему бы просто не использовать 1, а не 1.0? Если это вычисленный ключ - ошибка округления может гасить вещи:

>>> d = {}
>>> d[1] = 5
>>> d[1.0]
5
>>> x = sum(0.01 for i in range(100)) #conceptually this is 1.0
>>> d[x]
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    d[x]
KeyError: 1.0000000000000007

Итак, я бы сказал, что, вообще говоря, ответ на ваш вопрос "это всегда полезная языковая функция?" "Нет, наверное, нет".