Проверка наличия 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!