当我检查列表或集合中是否存在时,NaN处理得很好。但我不明白怎么做。 [更新:不,不是;如果找到相同的NaN实例,则报告存在;如果只发现不同的NaN实例,则报告为不存在。]
我认为列表中的存在是通过相等来测试的,所以我预计NaN不会被找到,因为NaN!= NaN。
hash(NaN)和hash(0)都是0.字典和集合如何告诉NaN和0分开?
使用in
运算符检查任意容器中NaN的存在是否安全?还是依赖于实现?
我的问题是关于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
的{{1}}行为似乎非常奇怪:
dict
我说'奇怪',因为我没有在文档中看到它解释(也许我错过了),我认为这不应该作为一个实现选择。
当然,一个NaN对象可能与另一个NaN对象不同(在def __contains__(self, x):
for element in self:
if x is element:
return True
if x == element:
return True
return False
意义上)。 (这并不奇怪; Python不保证这样的身份。实际上,我从未看到CPython共享在不同地方创建的NaN实例,即使它共享一个小数字或短字符串的实例。)这意味着在内置容器中测试NaN的存在是不确定的。
这非常危险,非常微妙。有人可能会运行我上面显示的代码,并且错误地认为使用id
测试NaN成员资格是安全的。
我认为这个问题没有一个完美的解决方法。一种非常安全的方法是确保NaN永远不会添加到内置容器中。 (在整个代码中检查它是一件痛苦的事情......)
另一种选择是注意左侧in
可能有NaN的情况,在这种情况下,使用in
分别测试NaN成员资格。此外,还需要避免或重写其他操作(例如,设置交集)。
答案 0 :(得分:3)
问题#1:为什么NaN在容器中找到,当它是一个相同的对象时。
对于容器类型,例如list,tuple,set,frozenset,dict或 collections.deque,y中的表达式x等价于任何(x是e 或x == e表示e)。
这正是我用NaN观察到的,所以一切都很好。为什么这个规则?我怀疑这是因为dict
/ set
想要诚实地报告它包含某个对象(如果该对象实际上在其中)(即使__eq__()
因任何原因选择报告该对象不等于自己。)
问题2:为什么NaN的哈希值与0相同?
由内置函数hash()调用,以及对成员的操作 哈希集合包括set,frozenset和dict。的散列强>() 应该返回一个整数。唯一必需的属性是对象 比较相等的哈希值相同;建议以某种方式 混合在一起(例如使用exclusive或)哈希值 对象的组件也在比较中起作用 对象。
请注意,要求仅在一个方向;具有相同散列的对象不必相等!起初我认为这是一个错字,但后来我意识到它不是。无论如何,即使使用默认__hash__()
,也会发生散列碰撞(请参阅优秀解释here)。容器处理碰撞没有任何问题。当然,它们最终使用==
运算符来比较元素,因此只要它们不相同,它们就很容易得到多个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。
最后,我在文档here中发现了一个错误:
对于未定义
__contains__()
的用户定义的类,但确实如此 定义__iter__()
,如果某个值x in y
与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
如您所见,首先在==
之前检查身份 - 与内置容器一致。我会提交一份报告来修复文档。
答案 1 :(得分: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
我相信元组/集查找具有与相同对象的比较相关的一些优化。
回答你的问题 - 在检查in
是否存在的同时,在NaN
运营商上转发是不安全的。如果可能,我建议使用None
。
只是评论。 __eq__
与is
语句无关,在查找过程中,对象ID的比较似乎发生在任何值比较之前:
>>> 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!