检查容器中是否存在NaN

发布于 2021-01-29 17:12:54

当我检查列表或集合中的NaN时,NaN会得到完美处理。但我不知道如何。[更新:不,不是;如果找到相同的NaN实例,则报告为存在;如果仅找到不相同的NaN实例,则将其报告为不存在。]

  1. 我认为列表中的存在是通过相等性测试的,所以我希望因为NaN!= NaN而找不到NaN。

  2. hash(NaN)和hash(0)均为0。字典和集合如何区分NaN和0?

  3. 使用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使用operator来测试遏制的原因==

编辑:

每罗马的答案,这似乎是__contains__listtuplesetdict在一个非常奇怪的方式行事:

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()。另外,还需要避免或重写其他操作(例如,设置交集)。

关注者
0
被浏览
39
1 个回答
  • 面试哥
    面试哥 2021-01-29
    为面试而生,有面试问题,就找面试哥。

    问题#1:为什么在同一对象中的容器中会发现NaN。

    文档中

    对于列表,元组,集合,frozenset,dict或collections.deque等容器类型,y中的表达式x等于any(x为e或y中的e为x ==
    e)。

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

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

    文档中

    由内置函数hash()调用,用于对散列集合的成员(包括set,frozenset和dict)进行操作。 hash
    ()应该返回一个整数。唯一需要的属性是比较相等的对象具有相同的哈希值;建议以某种方式将散列值混合在一起(例如,使用异或),以将对象的组成部分也用作对象比较的一部分。

    请注意,此要求仅在一个方向上进行;具有相同散列的对象不必相等!起初我以为是错字,但后来我意识到不是。哈希冲突无论如何都会发生,即使默认情况下也是如此__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如果一些值为truezx == z同时迭代产生y。如果在迭代过程中in引发了异常,则好像引发了该异常。

    最后,尝试使用旧式的迭代协议:如果一个类定义 __getitem__()x in y则当且仅当存在一个非负整数索引(i例如)x == y[i],并且所有较低的整数索引都不会引发IndexError异常时,该方法才为true
    。(如果引发了其他任何异常,则好像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
    

    如您所见,先检查身份,然后再检查==-与内置容器一致。我将提交报告以修复文档。



知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看