Проверка наличия NaN в контейнере - PullRequest
12 голосов
/ 28 марта 2012

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

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

  2. hash (NaN) и hash (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 сдерживание проверяется с использованием оператора ==.

РЕДАКТИРОВАТЬ:

В ответе Романа может показаться, что __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 ]

3 голосов
/ 05 апреля 2012

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

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

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

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

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

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

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

Обратите внимание, что требование только в одном направлении;объекты с одинаковым хешем не обязательно должны быть равны!Сначала я подумал, что это опечатка, но потом понял, что это не так.В любом случае коллизии хэшей случаются, даже если по умолчанию __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 получается при итерации по y.Если во время итерации возникает исключение, это как если бы in вызывало это исключение.

Наконец, используется итерационный протокол старого стиля: если класс определяет __getitem__(), x in y - это истинатогда и только тогда, когда существует неотрицательный целочисленный индекс 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

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

2 голосов
/ 28 марта 2012

Я не могу воспроизвести ваши кортежи / наборы дел, используя 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

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

Отвечая на ваш вопрос - опасно ретранслировать оператор 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!
...