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

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

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

  2. хэш (NaN) и хэш(0) равны 0. Как словари и наборы различают NaN и 0?

  3. безопасно ли проверять наличие 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 во встроенном контейнере не определено.

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

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

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

2 ответов


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

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

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

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

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

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

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

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

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

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

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

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

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

наконец, используется протокол итерации старого стиля: если класс определяет __getitem__(), x in y true если и только если существует неотрицательное целочисленный индекс i такое, что x == y[i], и все более низкие целочисленные индексы делают не поднимать 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

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


я не могу повторить вам кортеж / набор случаев, используя 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 lookups имеет некоторую оптимизацию, связанную с сравнением одних и тех же объектов.

отвечая на ваш вопрос - это шов, чтобы быть небезопасным для реле на 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!