Проверка наличия NaN в контейнере

NaN отлично обрабатывается, когда я проверяю его присутствие в списке или наборе. Но я не понимаю, как это сделать. [ОБНОВЛЕНИЕ: нет. он сообщается как присутствующий, если найден идентичный экземпляр NaN; если найдены только неидентичные экземпляры NaN, сообщается как отсутствует.]

  • Я думал, что присутствие в списке проверено на основе равенства, поэтому я ожидал, что NaN не будет найден, поскольку NaN!= NaN.

  • hash (NaN) и hash (0) равны 0. Как словари и множества говорят NaN и 0 отдельно?

  • Безопасно ли проверять присутствие NaN в произвольном контейнере с помощью оператора in? Или это зависит от реализации?

Мой вопрос касается Python 3.2.1; но если в будущих версиях есть какие-либо изменения, существующие/запланированные, я тоже хотел бы это знать.

NaN = float('nan')
print(NaN != NaN) # True
print(NaN == NaN) # False

list_ = (1, 2, NaN)
print(NaN in list_) # True; works fine but how?

set_ = {1, 2, NaN}
print(NaN in set_) # True; hash(NaN) is some fixed integer, so no surprise here
print(hash(0)) # 0
print(hash(NaN)) # 0
set_ = {1, 2, 0}
print(NaN in set_) # False; works fine, but how?

Обратите внимание, что если я добавлю экземпляр определяемого пользователем класса в list, а затем проверим на наличие сдерживания, вызывается метод экземпляра __eq__ (если он определен) - по крайней мере, в CPython. Поэтому я предположил, что сдерживание list проверяется с помощью оператора ==.

EDIT:

В ответ на романа, кажется, что __contains__ для list, tuple, set, dict ведет себя очень странно:

def __contains__(self, x):
  for element in self:
    if x is element:
      return True
    if x == element:
      return True
  return False

Я говорю "странно", потому что я не видел, чтобы это объяснялось в документации (возможно, я пропустил это), и я думаю, что это то, что не следует оставлять в качестве варианта реализации.

Конечно, один объект NaN может быть не идентичным (в смысле id) другому объекту NaN. (Это не удивительно: Python не гарантирует такую ​​идентичность. Фактически, я никогда не видел, чтобы CPython совместно использовал экземпляр NaN, созданный в разных местах, хотя он имеет экземпляр небольшого числа или короткую строку.) Это означает, что тестирование присутствия NaN во встроенном контейнере undefined.

Это очень опасно и очень тонко. Кто-то может запустить сам код, который я показал выше, и неправильно заключить, что он безопасен для тестирования членства NaN с помощью in.

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

Другая альтернатива - следить за случаями, когда in может иметь NaN с левой стороны, а в таких случаях тестировать членство NaN отдельно, используя math.isnan(). Кроме того, необходимо также избегать или переписывать другие операции (например, установить пересечение).

Ответ 1

Вопрос №1: почему NaN найден в контейнере, когда он является идентичным объектом.

Из документация:

Для типов контейнеров, таких как список, кортеж, набор, frozenset, dict или collection.deque, выражение x в y эквивалентно любому (x есть e или x == e для e в y).

Именно это я наблюдаю с NaN, так что все в порядке. Почему это правило? Я подозреваю это, потому что dict/set хочет честно сообщить, что он содержит определенный объект, если этот объект на самом деле находится в нем (даже если __eq__() по какой-либо причине хочет сообщить, что объект не равен самому себе).

Вопрос №2: почему хэш-значение для NaN такое же, как для 0?

Из документация:

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

Обратите внимание, что это требование находится только в одном направлении; объекты, имеющие один и тот же хэш, не обязательно должны быть равны! Сначала я подумал, что это опечатка, но потом я понял, что это не так. В любом случае, столкновение хэшей происходит даже с дефолтом __hash__() (см. Отличное объяснение здесь). Контейнеры обрабатывают столкновение без каких-либо проблем. Разумеется, они, конечно, используют оператор == для сравнения элементов, поэтому они могут легко получить множество значений NaN, если они не идентичны! Попробуйте следующее:

>>> nan1 = float('nan')
>>> nan2 = float('nan')
>>> d = {}
>>> d[nan1] = 1
>>> d[nan2] = 2
>>> d[nan1]
1
>>> d[nan2]
2

Итак, все работает как задокументировано. Но... это очень опасно! Сколько людей знали, что несколько ценностей NaN могут жить рядом друг с другом в dict? Сколько людей найдет это легко отлаживаемым?..

Я бы рекомендовал сделать NaN экземпляром подкласса float, который не поддерживает хэширование и, следовательно, не может быть случайно добавлен к set/dict. Я отправлю это в идеи python.

Наконец, я обнаружил ошибку в документации здесь:

Для пользовательских классов, которые не определяют __contains__(), но делают define __iter__(), x in y истинно, если какое-либо значение z с x == z равно производится при итерации над y. Если во время iteration, это как если бы in вызвало это исключение.

Наконец, проверяется протокол итерации старого стиля: если класс определяет __getitem__(), x in y истинно тогда и только тогда, когда существует неотрицательный целочисленный индекс i такой, что x == y[i], и все нижние целые индексы do не поднимать IndexError исключение. (Если возникает какое-либо другое исключение, как будто in вызвало это исключение).

Вы можете заметить, что здесь нет упоминания is, в отличие от встроенных контейнеров. Я был удивлен этим, поэтому я попробовал:

>>> nan1 = float('nan')
>>> nan2 = float('nan')
>>> class Cont:
...   def __iter__(self):
...     yield nan1
...
>>> c = Cont()
>>> nan1 in c
True
>>> nan2 in c
False

Как вы можете видеть, идентификатор сначала проверяется, а до == - в соответствии со встроенными контейнерами. Я отправлю отчет, чтобы исправить документы.

Ответ 2

Я не могу повторить загрузку/набор случаев с помощью float('nan') вместо NaN.

Поэтому я предполагаю, что он работал только потому, что id(NaN) == id(NaN), то есть нет интернирования для объектов NaN:

>>> NaN = float('NaN')
>>> id(NaN)
34373956456
>>> id(float('NaN'))
34373956480

и

>>> NaN is NaN
True
>>> NaN is float('NaN')
False

Я считаю, что поиск в tuple/set имеет некоторую оптимизацию, связанную с сопоставлением одних и тех же объектов.

Отвечая на ваш вопрос - этот шов небезопасен для ретрансляции на in при проверке наличия NaN. Я бы рекомендовал использовать None, если это возможно.


Просто комментарий. __eq__ не имеет ничего общего с оператором is, а во время поиска сравнение идентификаторов объектов, похоже, происходит до любых сопоставлений значений:

>>> class A(object):
...     def __eq__(*args):
...             print '__eq__'
...
>>> A() == A()
__eq__          # as expected
>>> A() is A()
False           # `is` checks only ids
>>> A() in [A()]
__eq__          # as expected
False
>>> a = A()
>>> a in [a]
True            # surprise!