Python 3.7 中的缓存整数、“is”运算符和“id()”

Cached integers, the `is` operator and `id()` in Python 3.7

在我的 Python 谈话中,当谈到一些 Python 琐事时,我曾经展示过类似 print(5 is 7 - 2, 300 is 302 - 2) 的东西。今天我意识到这个例子在 Python 3.7.

中 运行 时产生了(对我来说)意想不到的结果

我们知道从 -5 到 255 的数字在内部缓存 Python 3 docs - PyLong_FromLong 这也可以在早期的 API 文档中找到。

is 运算符(如文档 Python 3 docs - is operator 中所述)测试对象身份,即它使用 id() 函数来确定并在以下情况下产生 True值匹配。

id() 函数保证 运行 为对象在其生命周期内 return 一个唯一且恒定的值(在文档 Python 3 docs - id() 中也有描述)。

所有这些规则为您提供了以下结果(许多 Python 编码人员都知道):

Python 2.7:

>>> print(5 is 7 - 2, 300 is 302 - 2)
True False

Python 3.6:

>>> print(5 is 7 - 2, 300 is 302 - 2)
True False

但是,Python 3.7 的行为有所不同:

>>> print(5 is 7 - 2, 300 is 302 - 2)
True True

我试图理解原因,但我在 Python 来源中找不到任何提示...

id(302 - 2) 总是产生不同的值,所以我想知道为什么 302 - 2 is 300 会产生 Trueis 运算符如何知道值相同? Python 3.7 中的整数比较是否以某种方式超载?

>>> id(300)
140059023515344

>>> id(302 - 2)
140059037091600

>>> id(300) is id(302 - 2)
False

>>> 300 is 302 - 2
True

>>> id(300) == id(302 -2)
True

>>> id(302 - 2)
140059037090320

>>> id(302 - 2)
140059023514640

is 没有改变。语言语义的任何部分都没有改变;您正在比较的对象是否是同一对象从未指定行为。 is 比较的两侧现在恰好是同一个对象。这是常量折叠优化变化的影响。

代码对象的初始生成 co_consts 将单个对象重新用于等效的原子常量。 (我说 "equivalent" 而不是 "equal" 因为 1 和 1.0 不等价。)这与缓存从 -5 到 256 的整数的效果不同,它只适用于单个代码对象. Previously,将 302 - 2 转换为 300 的编译时优化过程发生在字节码窥孔优化器中,它在初始 co_consts 生成后启动,并且不执行相同的常量重用。

在 CPython 3.7 中,此优化过程是 moved 从字节码窥孔优化器到新的 AST 优化器。 AST 优化器在初始生成代码对象 co_consts 之前生效,因此常量重用现在适用于结果。


您可以通过执行类似

的操作来查看对旧 Python 版本不断重用的影响
>>> 300 is 300
True

即使在 CPython 2.7 或 3.6 上也会产生 True,尽管 300 超出了小整数缓存的范围。您可以通过确保您正在比较的常量最终出现在单独的代码对象中来防止常量重用:

>>> (lambda: 300)() is 300
False

这会在任何版本的 CPython 上产生 False,即使有新的优化器更改也是如此。然而,它在 PyPy 上产生 True,因为 PyPy 有自己的优化行为,PyPy 的行为就好像所有相等的整数都由同一个整数对象表示。