【问题标题】:'is' operator behaves unexpectedly with floats'is' 运算符对浮点数的行为异常
【发布时间】:2016-12-14 13:22:15
【问题描述】:

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

==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) 的内部表示应该相等。与== 的比较按预期工作。

【问题讨论】:

  • 您的问题值得回答,但如果您在实际代码中遇到此问题,则代码可能有误,应予以修复。 (几乎)没有理由以这种方式测试浮点数之间的引用身份。
  • 奇怪的是,虽然我可以重现这一点,但 id(0.0)id(float(0.0))id(float(0)) 都返回相同的值。 ...也就是说,如果我在交互式 shell 中一个接一个地执行这些值,那么值是相同的,但是如果我执行 id(float(0.0)), id(float(0)) (作为一个元组),那么 ID 就会不同。有什么解释吗?
  • @Elazar:感谢添加 cpython 标记,因为它似乎与 cpython 非常相关,您不应该依赖任何东西(正如您已经提到的)
  • @tobias_k:两个原因:代码中的不可变文字与代码对象一起存储为常量(因此0.0 is 0.0 只生成一个可重用的对象),Python 重用内存(因此id(0.0) 后跟另一个id(someobject) 可以很容易地产生相同的 id,因为前一个已经被垃圾回收了)并且产生一个元组不能重用内存位置,因为你仍然需要 all 对象作为其中的一部分元组。

标签: python python-2.7 python-3.x floating-point python-internals


【解决方案1】:

这与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 的结果也取决于它正在执行的范围(超出此问题/解释的范围),请参考用户:@@ 987654321@code objects 的精彩解释。甚至 python 的文档也包含有关此行​​为的部分:

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

【讨论】:

  • float(17.0) is float(17.0) 返回 Truefloat(17.0) is float('17.0') 返回 False -> 是 float( some_value ) 只是返回原始浮点数,因此不会生成新实例(如果 some_value 是浮点数... )?
  • @Jim 回答了这个问题
  • CPython 不缓存浮点数。对于is 给出True 的情况,您会看到常量折叠和float 构造函数直接传递浮点数的组合。
  • CPython 实现还将整数/浮点数缓存为该范围内的单例对象 -> [x >= -5 | x 。不,只有 int 对象被实习。这里发生的情况是 literals 与代码对象一起存储为常量,在同一个代码对象常量数组中存储两个 0.0 值是没有意义的。
  • 试用compile('0.0 is 0.0', '', 'exec').co_consts;你会发现一个 0.0 对象和None
【解决方案2】:

如果将float 对象提供给float()CPython* 只会返回它而不创建新对象。

这可以在PyNumber_Float(最终从float_new调用)中看到,其中传入的对象oPyFloat_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 使用相同的对象,因为它们是the same code object 的一部分(简短的免责声明:它们在同一逻辑行上);所以is 测试会成功。

如果您在单独的行中执行float(0.0)(或由; 分隔)并然后检查身份,则可以进一步证实这一点:

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

另一方面,如果提供了int(或str),CPython 将从它创建一个float 对象并返回它。为此,它分别使用PyFloat_FromDoublePyFloat_FromString

效果是返回的对象在ids 不同(以前用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) 

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

【讨论】:

  • 感谢您提供详细信息的回答,尽管我会接受 mrdomoboto 的回答是正确的(我讨厌这个艰难的决定......)
  • 我在代码对象上添加了你的部分作为脚注(顺便说一句很好的解释!)。
猜你喜欢
  • 2010-09-23
  • 2022-10-07
  • 2016-03-12
相关资源
最近更新 更多