is" 运算符在整数中表现异常

617 投票
11 回答
108152 浏览
提问于 2025-04-11 18:57

为什么下面的代码在Python中表现得有点奇怪呢?

>>> a = 256
>>> b = 256
>>> a is b
True           # This is an expected result
>>> a = 257
>>> b = 257
>>> a is b
False          # What happened here? Why is this False?
>>> 257 is 257
True           # Yet the literal numbers compare properly

我在使用Python 2.5.2。尝试了不同版本的Python后,发现Python 2.3.3在99和100之间的表现和上面说的一样。

根据以上情况,我可以猜测Python内部的实现方式是“小”的整数和较大的整数存储方式不同,而is这个操作符可以区分它们。为什么会有这种不一致的情况呢?有没有更好的方法来比较两个不确定是否是数字的对象,以判断它们是否相同呢?

11 个回答

78

我来晚了,不过你想要一些源代码来支持你的答案吗? 我会尽量用简单的方式来表达,这样更多的人可以理解。


CPython的一个好处是你可以看到它的源代码。我将使用3.5版本的链接,但找到对应的2.x版本也很简单。

在CPython中,处理创建新int对象的C-API函数是 PyLong_FromLong(long v)。这个函数的描述是:

当前的实现会保留一个整数对象的数组,范围是-5到256,当你在这个范围内创建一个int时,实际上你只是得到了一个对现有对象的引用。所以应该可以改变1的值。我怀疑在这种情况下Python的行为是未定义的。:-)

(我的强调)

我不知道你怎么想,但我看到这个就想:我们来找找那个数组吧!

如果你还没有尝试过修改实现CPython的C代码你应该试试;一切都很有条理且易于阅读。对于我们的情况,我们需要查看Objects子目录,它位于主源代码目录树中。

PyLong_FromLong处理long对象,所以我们可以推测需要查看longobject.c。看里面的时候你可能会觉得有点混乱;确实是这样,但别担心,我们要找的函数在第230行等着我们去查看。这个函数不大,主要内容(不包括声明)很容易粘贴在这里:

PyObject *
PyLong_FromLong(long ival)
{
    // omitting declarations

    CHECK_SMALL_INT(ival);

    if (ival < 0) {
        /* negate: cant write this as abs_ival = -ival since that
           invokes undefined behaviour when ival is LONG_MIN */
        abs_ival = 0U-(unsigned long)ival;
        sign = -1;
    }
    else {
        abs_ival = (unsigned long)ival;
    }

    /* Fast path for single-digit ints */
    if (!(abs_ival >> PyLong_SHIFT)) {
        v = _PyLong_New(1);
        if (v) {
            Py_SIZE(v) = sign;
            v->ob_digit[0] = Py_SAFE_DOWNCAST(
                abs_ival, unsigned long, digit);
        }
        return (PyObject*)v; 
}

现在,我们不是C语言的高手,但也不傻,我们可以看到CHECK_SMALL_INT(ival);在向我们招手;我们可以理解它和这个有关。我们来看看:

#define CHECK_SMALL_INT(ival) \
    do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \
        return get_small_int((sdigit)ival); \
    } while(0)

所以这是一个宏,如果值ival满足条件,就会调用函数get_small_int

if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS)

那么NSMALLNEGINTSNSMALLPOSINTS是什么呢?它们是宏!在这里可以找到

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

所以我们的条件是if (-5 <= ival && ival < 257)时调用get_small_int

接下来我们来看get_small_int的具体实现(好吧,我们只看它的主体,因为那里才是有趣的地方):

PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);

好的,声明一个PyObject,确保之前的条件成立,然后执行赋值:

v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];

small_ints看起来就像我们一直在寻找的那个数组,没错!我们本可以直接看文档就知道了!

/* Small integers are preallocated in this array so that they
   can be shared.
   The integers that are preallocated are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

没错,这就是我们要找的。当你想在范围[NSMALLNEGINTS, NSMALLPOSINTS)内创建一个新的int时,你实际上只是得到了一个已经存在的对象的引用,这个对象是预先分配好的。

因为这个引用指向的是同一个对象,所以直接调用id()或者用is来检查它的身份都会返回完全相同的结果。

那么,它们是什么时候被分配的呢??

在初始化时,_PyLong_Init,Python会很乐意进入一个循环来为你完成这件事:

for (ival = -NSMALLNEGINTS; ival <  NSMALLPOSINTS; ival++, v++) {

查看源代码以阅读循环的主体!

我希望我的解释让你对的理解更加清晰了(这个双关语是故意的)。


但是,257是257? 这是怎么回事?

这个其实更容易解释,我之前已经尝试过解释了;这是因为Python会将这个交互式语句作为一个整体来执行:

>>> 257 is 257

在编译这个语句时,CPython会看到你有两个相同的字面量,并会使用同一个PyLongObject来表示257。如果你自己进行编译并检查其内容,就能看到这一点:

>>> codeObj = compile("257 is 257", "blah!", "exec")
>>> codeObj.co_consts
(257, None)

当CPython执行这个操作时,它现在只会加载完全相同的对象:

>>> import dis
>>> dis.dis(codeObj)
  1           0 LOAD_CONST               0 (257)   # dis
              3 LOAD_CONST               0 (257)   # dis again
              6 COMPARE_OP               8 (is)

所以is会返回True

160

Python中的“is”运算符在处理整数时表现得很奇怪?

总结一下——我想强调的是:不要用 is 来比较整数。

这种行为是你不应该有任何期待的。

相反,使用 ==!= 来比较相等和不相等。例如:

>>> a = 1000
>>> a == 1000       # Test integers like this,
True
>>> a != 5000       # or this!
True
>>> a is 1000       # Don't do this! - Don't use `is` to test integers!!
False

解释

要理解这一点,你需要知道以下几点。

首先,is 是干什么的?它是一个比较运算符。根据文档

运算符 isis not 用于测试对象的身份:x is y 只有在 x 和 y 是同一个对象时才为真。x is not y 则返回相反的真值。

因此,以下是等价的。

>>> a is b
>>> id(a) == id(b)

根据文档

id 返回一个对象的“身份”。这是一个整数(或长整数),在对象的生命周期内是唯一且不变的。两个生命周期不重叠的对象可能会有相同的 id() 值。

请注意,在 CPython(Python 的参考实现)中,对象的 id 是内存中的位置,这只是一个实现细节。其他 Python 实现(如 Jython 或 IronPython)可能会有不同的 id 实现。

那么 is 的使用场景是什么呢? PEP8 说明

与单例(如 None)的比较应该始终使用 isis not,而不是使用相等运算符。

问题

你问了以下问题(附带代码):

为什么以下代码在 Python 中表现得很奇怪?

>>> a = 256
>>> b = 256
>>> a is b
True           # This is an expected result

这不是一个预期的结果。为什么会这样?这仅仅意味着两个值为 256 的整数被 ab 引用的是同一个整数实例。整数在 Python 中是不可变的,因此它们不能改变。这对任何代码都不应该有影响。这不应该被期待。这只是一个实现细节。

但也许我们应该感到高兴,因为每次声明一个值等于 256 时,内存中不会创建一个新的独立实例。

>>> a = 257
>>> b = 257
>>> a is b
False          # What happened here? Why is this False?

看起来我们现在在内存中有两个值为 257 的整数实例。由于整数是不可变的,这会浪费内存。希望我们不会浪费太多内存。我们可能不会。但这种行为并没有保证。

>>> 257 is 257
True           # Yet the literal numbers compare properly

好吧,这看起来是你使用的特定 Python 实现试图聪明地避免在内存中创建冗余的整数,除非必须。你似乎表明你使用的是 Python 的参考实现,即 CPython。CPython 做得不错。

如果 CPython 能够在全局范围内做到这一点,并且成本不高(因为查找会有成本),也许其他实现会这样做。

但就代码的影响而言,你不应该关心一个整数是否是某个特定整数的实例。你只需要关心该实例的值,而你会使用普通的比较运算符来进行比较,也就是 ==

is 的作用

is 检查两个对象的 id 是否相同。在 CPython 中,id 是内存中的位置,但在其他实现中可能是其他唯一标识的数字。用代码重新表述一下:

>>> a is b

>>> id(a) == id(b)

那么我们为什么还要使用 is 呢?

这可以相对快速地检查,比如说,检查两个非常长的字符串是否相等。但由于它适用于对象的唯一性,因此它的使用场景有限。实际上,我们主要想用它来检查 None,它是一个单例(在内存中唯一存在的实例)。如果有可能混淆它们,我们可能会创建其他单例,这时我们可能会用 is 来检查,但这些情况相对少见。这里有一个例子(在 Python 2 和 3 中都能工作):

SENTINEL_SINGLETON = object() # this will only be created one time.

def foo(keyword_argument=None):
    if keyword_argument is None:
        print('no argument given to foo')
    bar()
    bar(keyword_argument)
    bar('baz')

def bar(keyword_argument=SENTINEL_SINGLETON):
    # SENTINEL_SINGLETON tells us if we were not passed anything
    # as None is a legitimate potential argument we could get.
    if keyword_argument is SENTINEL_SINGLETON:
        print('no argument given to bar')
    else:
        print('argument to bar: {0}'.format(keyword_argument))

foo()

这将打印:

no argument given to foo
no argument given to bar
argument to bar: None
argument to bar: baz

因此我们看到,使用 is 和一个哨兵,我们能够区分 bar 在没有参数时被调用和传入 None 时的情况。这是 is 的主要使用场景——不要 用它来测试整数、字符串、元组或其他类似的东西的相等性。

458

看看这个:

>>> a = 256
>>> b = 256
>>> id(a) == id(b)
True
>>> a = 257
>>> b = 257
>>> id(a) == id(b)
False

在关于“普通整数对象”的文档中,我发现了以下内容:

当前的实现方式是,系统会为所有在-5256之间的整数保留一个整数对象的数组。当你创建一个这个范围内的整数时,其实你得到的只是对已有对象的引用。

所以,整数256是完全相同的,但257就不是了。这是CPython的实现细节,其他Python的实现可能不一定这样。

撰写回答