Проверка наличия NaN в контейнере
NaN обрабатывается отлично, когда я проверяю его присутствие в списке или наборе. Но я не понимаю, как. [UPDATE: нет, это не так; сообщается, что он присутствует, если найден идентичный экземпляр NaN; если найдены только не идентичные экземпляры NaN, сообщается, что он отсутствует.]
я думал, что присутствие в списке проверяется равенством, поэтому я ожидал, что NaN не будет найден с NaN != значение NaN.
хэш (NaN) и хэш(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 во встроенном контейнере не определено.
это очень опасно и очень тонко. Кто-то может запустить тот самый код, который я показал выше, и неправильно заключить, что безопасно тестировать членство в NaN с помощью in
.
я не думаю, что есть идеальное решение этой проблемы. Один, очень безопасный подход, заключается в обеспечении того, чтобы NaN никогда не добавлялись во встроенные контейнеры. (Это боль, чтобы проверить это по всему коду...)
другая альтернатива-следить за случаями, когда in
может иметь NaN с левой стороны и в такие случаи, тест для членства NaN отдельно, используя math.isnan()
. Кроме того, необходимо также избегать или переписывать другие операции (например, пересечение множеств).
2 ответов
Вопрос №1: Почему NaN находится в контейнере, когда это идентичный объект.
С документация:
dict/для типов контейнеров, таких как list, tuple, set, frozenset, dict или коллекции.deque, выражение x в y эквивалентно любому (x равно e или x == e для e в y).
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!