为什么 (0-6) 等于 -6 是错误的?

131

在调试一些代码时,我发现了一些奇怪的东西。 显然,

>>> (0-6) is -6
False

但是,

>>> (0-5) is -5
True

为什么会发生这种情况?


27
你为什么在第一次使用中用了 is?除了 is/is not None 的情况外,在Python中通常不应经常使用它。 - Russell Borogove
4
@Russell的评论说得很对——问题在于某人显然在比较数字时使用了“is”,并期望它能像=一样运作,这是一个错误的期望。 - LarsH
4个回答

155

从-5到256的所有整数都作为全局对象缓存,并与CPython共享相同的地址,因此is测试通过。

这个特性在http://www.laurentluce.com/posts/python-integer-objects-implementation/中有详细解释,我们可以在http://hg.python.org/cpython/file/tip/Objects/longobject.c中检查当前的源代码。

一个特定的结构用于引用小整数并共享它们,因此访问速度很快。它是一个包含262个指向整数对象的指针的数组。这些整数对象在初始化期间分配在我们上面看到的整数对象块中。小整数范围是从-5到256。许多Python程序在使用该范围内的整数时花费了大量时间,因此这是一个明智的决定。

这只是CPython的一种实现细节,您不应该依赖它。例如,PyPy实现了返回数字本身的整数id,因此(0-6) is -6始终为真,即使它们在内部是“不同的对象”;它还允许您配置是否启用此整数缓存,甚至设置下限和上限。但是通常来说,从不同来源检索到的对象将不相同。如果您想比较相等性,请使用==


1
有趣的是,偏向正数。文章说“许多Python程序在使用该范围内的整数时花费了大量时间”,因此开发人员可能已经以某种方式进行了测量。我猜负数字面值现在只用于错误代码... - Frédéric Hamidi
请注意,PyPy在is上有不同的承诺(尽管不进行缓存)-http://pypy.readthedocs.org/en/latest/cpython_differences.html#object-identity-of-primitive-values-is-and-id - fijal
只是一点小提醒:引用帖子中有一个小错误,因此引文也有误(虽然答案的第一句话是正确的)——范围是-5到256,而不是257,因为零被视为正整数。 - kirelagin
3
@kirelagin也许就是这个意思。在Python中,range(m, n)表示[m, n)的整数区间,即m,m + 1,m + 2,...,n - 1。它不包括n,因此range(-5,257)不包含257,所以对于这个范围描述的行为是正确的。 - 0xc0de

30
Python在解释器中将整数存储在范围为-5至256的池中,从中返回这些整数。这就是为什么这些对象是相同的:(0-5)-5,但不是(0-6)-6,因为它们是即时创建的。
以下是CPython源代码中的源代码:
#define NSMALLPOSINTS           257
#define NSMALLNEGINTS           5
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

(查看 CPython 源代码/trunk/Objects/intobject.c)。源代码中包含以下注释:

/* References to small integers are saved in this array so that they
   can be shared.
   The integers that are saved are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
is操作符会比较它们(-5)是否相等,因为它们是同一个对象(相同的内存位置),但是另外两个新整数(-6)将位于不同的内存位置(然后is就不会返回True)。请注意,上面源代码中的257适用于正整数,因此为0-256(包括)。
(source)

27

这不是一个错误。 is 不是一个等式测试,== 才会给出期望的结果。

这种行为的技术原因是,Python 实现可以自由地将相同常量值的不同实例视为相同对象或不同对象。您正在使用的 Python 实现选择使某些小常量共享相同的对象,以节省内存。您不能依赖于此行为在版本之间或跨不同的 Python 实现中相同。


2
is 不是一个相等性测试。这个 is 是一个身份测试,用于查看两个对象是否完全相同。恰好在 CPython 实现中,一些 int 对象被缓存了。 - Casey Kuball

17

这是因为CPython缓存了一些小整数和小字符串,并为该对象的每个实例提供相同的id()

(0-5)-5id()方面具有相同的值,但0-6-6则不然。

>>> id((0-6))
12064324
>>> id((-6))
12064276
>>> id((0-5))
10022392
>>> id((-5))
10022392

同样地,对于字符串:

>>> x = 'abc'
>>> y = 'abc'
>>> x is y
True
>>> x = 'a little big string'
>>> y = 'a little big string'
>>> x is y
False

关于字符串缓存的更多细节,请阅读:is operator behaves differently when comparing strings with spaces


2
那么为什么-6被认为是“大”的,而-5不是呢?什么是被认为是“大”的资格标准? - inspectorG4dget
1
对于CPython,-5到256被“interned”(缓存)。这是一个有点武断的实现选择。如果给定的interned对象被广泛使用,可能会节省大量内存,但将其interning(在运行时或内存中)存在成本,因此你不想将其应用于所有内容。 - Russell Borogove
+1 表示显示 ID;我正要在我的答案中添加这个。 - Russell Borogove

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