NaNs как ключ в словарях

Может ли кто-нибудь объяснить мне следующее поведение?

>>> import numpy as np
>>> {np.nan: 5}[np.nan]
5
>>> {float64(np.nan): 5}[float64(np.nan)]
KeyError: nan

Почему это работает в первом случае, но не во втором? Кроме того, я обнаружил, что работают следующие DOES:

>>> a ={a: 5}[a]
float64(np.nan)

Ответ 1

Проблема заключается в том, что NaN не равно самому себе, как определено в стандарте IEEE для чисел с плавающей запятой:

>>> float("nan") == float("nan")
False

Когда словарь ищет ключ, он примерно делает это:

  • Вычислить хэш ключа для поиска.

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

    а. Проверка идентификатора объекта: если ключ в словаре и ключ, который нужно просмотреть, - это тот же объект, что и указатель оператора is, ключ найден.

    б. Если первая проверка не удалась, проверьте равенство с помощью оператора __eq__.

Первый пример преуспевает, так как np.nan и np.nan - это один и тот же объект, поэтому не имеет значения, что они не сравниваются равными:

>>> numpy.nan is numpy.nan
True

Во втором случае np.float64(np.nan) и np.float64(np.nan) не являются одним и тем же объектом - два вызова конструктора создают два разных объекта:

>>> numpy.float64(numpy.nan) is numpy.float64(numpy.nan)
False

Так как объекты также не сравниваются одинаково, словарь завершает, что ключ не найден и выбрасывает KeyError.

Вы можете даже сделать это:

>>> a = float("nan")
>>> b = float("nan")
>>> {a: 1, b: 2}
{nan: 1, nan: 2}

В заключение, кажется более разумной идеей избегать NaN в качестве словарного ключа.

Ответ 2

Пожалуйста, обратите внимание, что это не так больше в Python 3.6:

>>> d = float("nan") #object nan
>>> d
nan
>>> c = {"a": 3, d: 4}
>>> c["a"]
3
>>> c[d]
4

Как я понимаю:

c - это словарь, который содержит 3, связанные с "a", и 4, связанные с nan. Изменился внутренний взгляд на словарь в Python 3.6, теперь он сравнивает два указателя, и если они указывают на один и тот же объект, они считают, что равенство сохраняется. В противном случае они сравнивают хеш, если хеш отличается, то это не тот же объект. После этого, если все еще необходимо, ключи сравниваются "вручную".

Это означает, что хотя IEEE754 указывает, что NAN не равен самому себе:

>>> d == d
False

При поиске в словаре сначала учитываются указатели, и поскольку они указывают на один и тот же объект nan, он возвращает 4.

Обратите внимание, что:

>>> e = float("nan")
>>> e == d
False
>>> c[e]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: nan
>>> c[d]
4

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