关于NaN的两个案例分析

Lobsters Hottest 新闻

摘要

本文探讨了NaN在Python和Lua中的两种意外行为,其中对相等性和比较的隐含假设导致了令人惊讶的结果,例如Python列表相等性忽略了NaN的自不等性,以及Lua的for循环将NaN作为步长或限制时处理不当。

<p><a href="https://lobste.rs/s/v5hkjy/two_case_studies_nan">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/09 09:36

# NaN 的两个案例分析 来源:<https://sebsite.pw/w/20260709-nan.html> IEEE-754 的 NaN 很奇怪。正因如此,它常常被意外地忽略。我发现有两个例子,这种忽略渗入了编程语言的设计中。也就是说,这些语言的语义隐含了一些假设,而这些假设在遇到 NaN 时会被打破。 ## 案例一:Python ```python >>> from math import nan >>> nan == nan False >>> [nan] == [nan] True ``` 作为一种优化,在比较列表是否相等时,会先通过身份(identity)比较元素。只有当身份不同时,才会检查它们是否相等。通常,我们假定一个对象总是与自身相等。这在 Python 3 参考手册中有明确说明: > 用户自定义类如果定制了比较行为,应尽量遵循以下一致性规则: > - 相等比较应当具有自反性。换句话说,相同的对象应比较相等: > `x is y` 意味着 `x == y` 这对 NaN 不成立。这没问题,但因为这个假设对几乎所有其他情况都成立,所以设计上的选择使得 NaN 被忽略(或者至少让 NaN 表现出非常奇怪的行为)。需要说明的是,我并不一定认为 Python 这里的行为是“错误的”,或者这个优化因为 NaN 表现奇怪就很差。但我觉得至少值得指出这一点。 ## 案例二:Lua Lua 中的数值型 for 循环是这样写的: ```lua for i = 1, 10 do stuff() end ``` 它的工作方式是:`i` 从初始值(这里是 1)开始,每次迭代按步长(可选的第三个操作数,默认是 1)递增,直到新值大于上限(10)时循环终止。所以这个循环会迭代 10 次,从 1 到 10(包含两端)。 那么如果 NaN(`0/0`)混入其中会怎样?以下是参考实现(PUC-Rio Lua)中的行为: ```lua -- 执行一次 for i = 0/0, 10 do print(i) -- nan end -- 执行一次 for i = 0/0, 0/0 do print(i) -- nan end -- 从不执行 for i = 1, 10, 0/0 do print(i) end -- 执行一次 for i = 10, 1, 0/0 do print(i) -- 10.0 end ``` 读完这段代码,试着理解发生了什么。每一个结果都非常反直觉。 实际情况是:前两个循环只执行一次,因为 for 循环的检查实现为 `limit < init`,但后续迭代时检查的是 `idx <= limit`。所以 NaN 通过了第一次测试,但没有通过第二次测试。后两个例子表明,当步长为 NaN 时,它总是被当作负数处理,即使你使用 `math.abs(0/0)` 也是如此。这是因为检查步长是否为正的条件是 `0 > step`,而对 NaN 来说这个条件当然始终为假。 顺便一提,这些都没有文档说明,所以这很可能是真正的疏忽。但有趣的是:使用 NaN 会导致实现中使用的具体比较操作符泄露到解释器的行为中。 简单的解决方案是检测并禁止在数值型 for 循环中使用 NaN。Lua 已经会在步长为 0 时抛出错误,所以没有对 NaN 进行类似的检查很有趣。 ## 结论 我想 Chris Siebenmann 之前也写过关于 NaN 作为 Go 中 map 键的奇怪行为(<https://utcc.utoronto.ca/~cks/space/blog/programming/GoNaNsAsMapKeys>),所以这算是 NaN 奇异性的第三个案例了。 NaN 很奇怪。而这意味着其他东西和它一起时也会表现得很奇怪。

相似文章

我一直在思考空指针

Lobsters Hottest

作者探讨了空指针的概念,讨论了其语义角色,并提出可以像NaN装箱那样,使用其他无效指针值来存储额外信息。

log 在 PHP 和 Lua 中是非单调的

Lobsters Hottest

PHP 和 Lua 的 log 函数由于对以 10 为底(log10)和其他底数(ln)使用不同算法,可能产生非单调的结果,导致出现违反直觉的行为,即底数越大,对数值反而越大。

整数无故发生神秘变化,而本应无代码生成影响

The Old New Thing (Raymond Chen)

一位开发者发现,交换两个等效宏竟导致无关函数中出现意外的整数变化,这篇博客文章深入探究了这一谜团,并对某个大语言模型(LLM)关于控制流保护的解释提出了质疑。

Go 中过度的空指针检查

Lobsters Hottest

一篇博客文章讨论了 Go 中过度的空指针检查如何可能表明代码不清晰和错误处理实践不佳,主张早期失败并显式建模不可用的依赖。