为什么 float 对象与 "is" 运算符的行为不同?

Why does the float object behave differently with the "is" operator?

据我所知,cpython 实现为一些相同的值保留相同的对象以节省内存。例如,当我创建 2 个值为 hello 的字符串时,cpython 不会创建 2 个不同的 PyObject:

>>> s1 = 'hello'
>>> s2 = 'hello'
>>> s1 is s2
True

我听说过它的名字 string interning。当我尝试用其他 python 类型检查它时,我观察到几乎所有可散列(不可变)类型都是相同的:

>>> int() is int()
True
>>> str() is str()
True
>>> frozenset() is frozenset()
True
>>> bool() is bool()
True

几乎所有可变类型都是相反的(cpython 即使对于相同的值也会创建一个新的 PyObject):

>>> list() is list()
False
>>> set() is set()
False
>>> dict() is dict()
False

我认为这是因为我们可以对不可变对象使用相同的 PyObject 而不会出现任何问题。

当我看到 float 类型的行为与其他不可变类型不同时,我的问题就出现了:

>>> float() is float()
False

为什么不同?

可变对象总是创建一个新对象,否则数据将被共享。这里不多解释,就好像你在一个空列表中追加一个项目,你不希望所有的空列表都有那个项目。

不可变对象的行为方式完全不同:

  • 字符串得到 interned。如果它们小于 20 个字母数字字符,并且是静态的(代码中的常量、函数名称等),它们将被缓存并从为它们保留的特殊映射中访问。它是为了节省内存,但更重要的是用来进行更快的比较。 Python 在后台使用了大量需要字符串比较的字典访问操作。能够通过比较它们的内存地址而不是实际值来比较 2 个字符串(如属性或函数名称)是一个显着的运行时改进。

  • 布尔简单 return the same object。考虑到只有 2 个可用,一次又一次地创建它们毫无意义。

  • 默认为小整数(从 -5 到 256),are also cached。这些经常被使用,几乎无处不在。每当一个整数在该范围内时,CPython 只是 returns 相同的对象。

然而浮点数不被缓存。与 0-10 极为常见的整数不同,1.0 不一定比 2.00.1 更常用。这就是为什么 float() 只是 returns 一个新的浮点数。我们本可以优化空 float(),我们可以检查速度优势,但它可能不会产生这样的差异。

float(0.0) is float(0.0) 时,混乱开始出现。 Python 内置了大量优化:

  • 首先,const保存在每个函数的代码对象中。 0.0 is 0.0 只是指同一个对象。这是一个编译时优化。

  • 其次,float(0.0) 采用 0.0 对象,因为它是一个浮点数(不可变),所以它 simply returns it。如果它已经是一个浮点数,则无需创建新对象。

  • 最后,1.0 + 1.0 is 2.0 也可以。原因是1.0 + 1.0是在编译时计算出来的,然后引用同一个2.0对象:

    def test():
        return 1.0 + 1.0 is 2.0
    
    dis.dis(test)
      2           0 LOAD_CONST               1 (2.0)
                  2 LOAD_CONST               1 (2.0)
                  4 IS_OP                    0
                  6 RETURN_VALUE
    

    如你所见,没有加法运算。该函数是用指向完全相同的常量对象的结果编译的。

因此,虽然没有特定于浮点数的优化,但有 3 种不同的通用优化在起作用。它们的总和最终决定它是否是同一个对象。