检查容器中的NaN存在

时间:2012-03-28 09:31:08

标签: python python-3.x containers equality nan

当我检查列表或集合中是否存在时,NaN处理得很好。但我不明白怎么做。 [更新:不,不是;如果找到相同的NaN实例,则报告存在;如果只发现不同的NaN实例,则报告为不存在。]

  1. 我认为列表中的存在是通过相等来测试的,所以我预计NaN不会被找到,因为NaN!= NaN。

  2. hash(NaN)和hash(0)都是0.字典和集合如何告诉NaN和0分开?

  3. 使用in运算符检查任意容器中NaN的存在是否安全?还是依赖于实现?

  4. 我的问题是关于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__listtupleset的{​​{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成员资格。此外,还需要避免或重写其他操作(例如,设置交集)。

2 个答案:

答案 0 :(得分:3)

问题#1:为什么NaN在容器中找到,当它是一个相同的对象时。

来自documentation

  

对于容器类型,例如list,tuple,set,frozenset,dict或   collections.deque,y中的表达式x等价于任何(x是e   或x == e表示e)。

这正是我用NaN观察到的,所以一切都很好。为什么这个规则?我怀疑这是因为dict / set想要诚实地报告它包含某个对象(如果该对象实际上在其中)(即使__eq__()因任何原因选择报告该对象不等于自己。)

问题2:为什么NaN的哈希值与0相同?

来自documentation

  

由内置函数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 yz相同,则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!