in 操作符,float("NaN") 和 np.nan

37
我曾经认为 Python 中的 in 运算符是通过相等性检查 == 来检查某个集合中是否存在元素,因此 element in some_list 等价于 any(x == element for x in some_list)。例如:
True in [1, 2, 3]
# True because True == 1


1 in [1., 2., 3.]
# also True because 1 == 1.

然而,众所周知NaN不等于它自己。因此,我预期float("NaN") in [float("NaN")]应该是False。实际上确实是False

然而,如果我们使用numpy.nan代替float("NaN"),情况就大不相同:

import numpy as np
np.nan in [np.nan, 1, 2]
# True

但是,np.nan == np.nan仍然返回False

这怎么可能?np.nanfloat("NaN")有什么区别?in如何处理np.nan

2个回答

37
在Python中检查列表中是否有某个元素,首先进行的是对象标识测试,然后只有在对象不同的情况下才进行相等性测试。1 float("NaN") in [float("NaN")]返回False,因为比较涉及到两个不同NaN对象。因此标识测试返回False,接着相等性测试也返回False,因为NaN != NaN
然而,np.nan in [np.nan, 1, 2] 返回True,因为比较涉及到相同NaN对象。对象标识测试返回True,因此Python立即将该项识别为列表中的元素。
对于Python的许多其他内置容器类型(例如元组和集合),使用__contains__方法(通过in调用)实现了相同的检查。
1 至少在CPython中是这样的。这里的对象标识意味着对象在相同的内存地址上找到:对于列表的包含方法是使用PyObject_RichCompareBool执行的,该方法会在进行更复杂的对象比较之前快速比较对象指针。其他Python实现可能不同。

2
是的。nan = float("NaN"); nan in [nan] 会返回 True。谢谢! - Ilya V. Schurov
这样做(先判断身份再判断相等)有什么好处吗?为什么不直接检查相等性呢?我问这个问题是因为我一直以为NaN是唯一一个满足x是x但x!=x的对象,现在看到这个我想知道是否还有其他情况? - ayhan
2
@ayhan - 检查身份是一项相对廉价的操作(只需比较内存地址)。检查相等性可能会非常昂贵。 - John Y
2
我的一位教授曾经说过,如果不需要正确性,我可以任意加快速度。文档中提到:对于 x in s,如果 s 中有一个项目等于 x,则返回 True,否则返回 False。看起来像是一个 bug - 是文档还是实现有待商榷。考虑到后果,最好只记录 in 运算符实际执行的操作。 - Voo
@Voo 因为 nan 是唯一会导致问题的东西,所以显然它并不被认为是足够重要的。请参阅 PEP 754 的拒绝通知 该 PEP 已被拒绝。在开放了四年后,它未能产生足够的社区兴趣。 - Paul Panzer
@Paul,我认为对于任何定义了奇怪的等式运算符的自定义类,这种情况也会发生,但我同意这可能只是在实践中出现nan时才会成为问题。 - Voo

6

值得一提的是,NumPy数组的行为与预期相符:

a = np.array((np.nan,))
a[0] in a
# False

主题的变化:

[np.nan]==[np.nan]
# True
[float('nan')]==[float('nan')]
# False
{np.nan: 0}[np.nan]
# 0
{float('nan'): 0}[float('nan')]
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# KeyError: nan

其他的内容都在@AlexRiley的优秀回答中涵盖了。

有趣的是,它即使对于 dtype=object 也能按预期工作。 - Ilya V. Schurov
NumPy数组的“in”操作是通过(array == item).any()实现,正如你的答案所示。我猜开发人员被迫选择这种方法,因为NumPy数组不是对象引用的内部集合,所以无法比较它们的id。 - Alex Riley
没错,numpy似乎总是按值进行比较:a = np.array([None,[np.nan]]); a[1] in a 也是 False - Paul Panzer
我认为这是因为 a[1] == [np.nan] 比较的是包含相同 NaN 对象的两个列表,而列表相等性检查使用 PyObject_RichCompareBool 来比较项目(因此会检查 ids)。对于 a == [np.nan],调用的是 NumPy 数组的相等性方法,该方法不会(或无法)检查值的 ids,因此 a[1][np.nan] 不被视为相等。(我相信执行对象数组代码的位置在这里)。 - Alex Riley
2
就像我说的,我认为这是一个bug。当评估arr==item时(其中item是[5]),那么[5]会被广播到类似于(概念上)[5, 5, 5]的东西中,因此列表性质在翻译中丢失了。我已经在NumPy跟踪器上开了一个问题。 - Paul Panzer
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接