Почему ключ словаря с плавающей запятой перезаписывает целочисленный ключ с тем же значением?
Я работаю через 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
. Это немного опасно для меня, это всегда полезная функция языка?
Ответы
Ответ 1
Вы должны учитывать, что dict
имеет целью хранить данные в зависимости от логического числового значения, а не от того, как вы его представляли.
Разница между int
и float
на самом деле является лишь деталью реализации, а не концептуальной. В идеале единственным типом номера должно быть произвольное число точности с неограниченной точностью даже суб-единство... это, однако, трудно реализовать, не вдаваясь в проблемы... но может быть, это будет единственным будущим числовым типом для Python.
Таким образом, при использовании разных типов по техническим причинам Python пытается скрыть данные реализации, а преобразование int
→ float
выполняется автоматически.
Было бы гораздо более удивительно, если бы в программе 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
Итак, я бы сказал, что, вообще говоря, ответ на ваш вопрос "это всегда полезная языковая функция?" "Нет, наверное, нет".