“is”运算符在处理浮点数时表现出意外行为

12

在单元测试一个模块时,我遇到了一个令人困惑的问题。该模块实际上正在进行数值转换,而我想比较这些值。

使用==is进行比较存在区别(在某种程度上,我知道它们之间的差异)。

>>> 0.0 is 0.0
True   # as expected
>>> float(0.0) is 0.0
True   # as expected

到目前为止,一切都如预期,但这里是我的“问题”:

>>> float(0) is 0.0
False
>>> float(0) is float(0)
False

为什么?至少最后一个对我来说真的很令人困惑。float(0)float(0.0)的内部表示应该是相等的。使用==进行比较正常工作。


2
相关:https://dev59.com/ZHVC5IYBdhLWcg3w-WOs - Elazar
2
你的问题值得回答,但如果你在实际代码中遇到了这个问题,那么代码很可能是错误的,应该进行修复。在这种情况下,几乎没有理由测试浮点数之间的引用标识。 - Elazar
1
奇怪的是,尽管我可以重现这个问题,但 id(0.0)id(float(0.0))id(float(0)) 的值都相同。也就是说,如果我在交互式 shell 中依次执行它们,那么它们的值是相同的,但是如果我将它们作为元组执行 id(float(0.0)), id(float(0)),那么它们的 ID 就不同了。有什么解释吗? - tobias_k
1
@Elazar:感谢您添加cpython标签,因为它似乎与cpython相关,而且您已经提到了它不应该依赖于任何东西。 - Günther Jena
3
两个原因:代码中的不可变字面量被存储为与代码对象一起的常量(因此0.0是0.0只产生一个被重用的对象),Python 重用内存(因此id(0.0)后跟另一个id(someobject)可能会产生相同的 id,因为之前的已被垃圾回收),而生成元组无法重用内存位置,因为您仍然需要所有对象都成为该元组的一部分。 - Martijn Pieters
显示剩余3条评论
2个回答

25

这与is的工作方式有关。它检查引用而不是值。如果任一参数被分配给相同的对象,则返回True

在这种情况下,它们是不同的实例;float(0)float(0)具有相同的值==,但在Python中看来是不同的实体。CPython实现还将整数缓存为单例对象,在此范围内-> [x | x ∈ ℤ ∧ -5 ≤ x ≤ 256 ]

>>> 0.0 is 0.0
True
>>> float(0) is float(0)  # Not the same reference, unique instances.
False

在这个例子中,我们可以演示整数缓存原理

>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False

现在,如果浮点数被传递给 float(),那么浮点数字面值将直接返回(短路),因为使用相同的引用,不需要从现有的浮点数实例化一个新的浮点数:

>>> 0.0 is 0.0
True
>>> float(0.0) is float(0.0)
True

通过使用int(),这一点可以进一步证明:

>>> int(256.0) is int(256.0)  # Same reference, cached.
True
>>> int(257.0) is int(257.0)  # Different references are returned, not cached.
False
>>> 257 is 257  # Same reference.
True
>>> 257.0 is 257.0  # Same reference. As @Martijn Pieters pointed out.
True

然而,is 的结果也取决于它所执行的作用域(超出了本问题/解释的范围),请参考用户:@Jim代码对象方面的精彩解释。甚至Python文档中也包括了这种行为的一节:

[7] 由于自动垃圾收集、空闲列表和描述符的动态性质,您可能会注意到在某些使用is操作符的情况下出现看似不寻常的行为,例如涉及实例方法或常量之间的比较。检查它们的文档以获取更多信息。


float(17.0) is float(17.0) 返回 True,但是 float(17.0) is float('17.0') 返回 False -> float(some_value) 只是返回原始的浮点数,因此不会创建新的实例(如果 some_value 是浮点数的情况下)? - Günther Jena
@Jim 回答了这个问题。 - Günther Jena
1
CPython不会缓存浮点数。您看到的是常量折叠和“float”构造函数直接通过的情况下,is给出True的组合。 - user2357112
3
CPython实现在区间[x >= -5 | x <= 256]中也将整数/浮点数缓存为单例对象。不,只有'int'对象被内部化。这里发生的是字面值被存储为代码对象的常量,而在同一代码对象常量数组中存储两个'0.0'值是没有意义的。 - Martijn Pieters
1
尝试执行 compile('0.0 is 0.0', '', 'exec').co_consts;你会发现只有一个 0.0 对象和一个 None - Martijn Pieters
@user2357112 是的,你说得对,我已经相应地更新了我的答案。 - ospahiu

11
如果向float()提供了一个float对象,CPython*只会返回它而不会创建新的对象。
这可以在PyNumber_Float(最终从float_new调用)中看到,其中传入的对象o通过PyFloat_CheckExact进行检查;如果为True,则只增加其引用计数并返回它。
if (PyFloat_CheckExact(o)) {
    Py_INCREF(o);
    return o;
}

因此,对象的id保持不变。因此,表达式
>>> float(0.0) is float(0.0) 

简化为:

>>> 0.0 is 0.0

但是为什么会等于True呢?嗯,CPython有一些优化。

在这种情况下,它使用相同的对象来处理你命令中出现的两个0.0,因为它们是同一个code对象的一部分(简短的免责声明:它们在同一行逻辑上);因此,is测试将成功。

如果你在不同的行上执行float(0.0)(或者用;分隔),然后检查身份,这可以进一步证实:

a = float(0.0); b = float(0.0) # Python compiles these separately
a is b # False 

另一方面,如果提供了一个整数(或字符串),CPython 将从中创建一个新的浮点数对象并返回。为此,它分别使用 PyFloat_FromDoublePyFloat_FromString。返回的对象的 id 不同(用于使用 is 检查身份)。
# Python uses the same object representing 0 to the calls to float
# but float returns new float objects when supplied with ints
# Thereby, the result will be False
float(0) is float(0) 

*注意:所有先前提到的行为适用于Python在C中的实现,即CPython。其他实现可能表现出不同的行为。简而言之,不要依赖它。

谢谢您提供详细答案,虽然我会接受mrdomoboto的答案是正确的(我讨厌做出这种难决定...) - Günther Jena
1
我将你关于代码对象的部分作为脚注添加了进去(顺便说一句,解释得非常好!)。 - ospahiu

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