为什么下面的代码在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操作符可以区分两者。为什么会有漏洞的抽象?有什么更好的方法来比较两个任意物体来判断它们是否相同当我事先不知道它们是否是数字时?


当前回答

我迟到了,但你想要一些答案来源吗?我将试着以一种介绍性的方式来描述,这样更多的人可以跟上。


CPython的一个好处是你可以看到它的源代码。我将使用3.5版本的链接,但要找到对应的2。X个1是微不足道的。

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

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

(斜体)

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

如果你没有摆弄过实现CPython的C代码,你应该;一切都很有条理,可读。对于我们的例子,我们需要查看主源代码目录树的Objects子目录。

PyLong_FromLong处理的是长对象,因此不难推断我们需要查看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 master-code-haxxorz但我们也不笨,我们可以看到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)

什么是NSMALLNEGINTS和nsmallpoints ?宏!他们是:

#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, nsmallpoints)范围内创建一个新的int时,你只会得到一个已经存在的对象的引用,这个对象已经被预先分配了。

由于引用引用的是同一个对象,因此直接发出id()或检查与is的标识将返回完全相同的结果。

但是,什么时候分配呢?

在_PyLong_Init的初始化过程中,Python会很高兴地进入一个for循环来为你做这件事:

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。

其他回答

看看这个:

>>> a = 256
>>> b = 256
>>> id(a)
9987148
>>> id(b)
9987148
>>> a = 257
>>> b = 257
>>> id(a)
11662816
>>> id(b)
11662828

以下是我在“普通整数对象”的文档中找到的:

当前的实现为-5到256之间的所有整数保留了一个整数对象数组。当你在这个范围内创建一个int型时,你实际上只是得到了一个对现有对象的引用。

正如你可以检入源文件inobject .c一样,Python缓存小整数以提高效率。每次创建对小整数的引用时,引用的都是缓存的小整数,而不是新对象。257不是一个小整数,所以它是作为一个不同的对象计算的。

为了这个目的,最好使用==。

Is是恒等运算符(功能类似于id(a) == id(b));只是两个相等的数不一定是同一个物体。出于性能考虑,有些小整数会被记忆,所以它们往往是相同的(这可以做到,因为它们是不可变的)。

另一方面,PHP的===运算符被描述为检查相等性和type: x == y和type(x) == type(y),正如Paulo Freitas的注释所述。对于常见的数字,这就足够了,但与之不同的是,类以一种荒谬的方式定义__eq__:

class Unequal:
    def __eq__(self, other):
        return False

PHP显然也允许“内置”类(我指的是在C级实现的,而不是在PHP中)。稍微不那么荒谬的用法可能是timer对象,它每次作为数字使用时都有不同的值。这就是为什么你想要模拟Visual Basic的Now而不是用time。time()来显示它是一个求值,我不知道。

Greg Hewgill (OP)做了一个澄清性的评论:“我的目标是比较对象的同一性,而不是价值相等。除了数字,我想把对象的同一性视为价值相等。”

这将有另一个答案,因为我们必须将事物分类为数字,以选择是否与==或is进行比较。CPython定义了数字协议,包括PyNumber_Check,但这不能从Python本身访问。

我们可以尝试对已知的所有数字类型使用isinstance,但这不可避免地是不完整的。types模块包含一个StringTypes列表,但没有NumberTypes。从Python 2.6开始,内置的数字类有一个基类numbers。数字,但它有同样的问题:

import numpy, numbers
assert not issubclass(numpy.int16,numbers.Number)
assert issubclass(int,numbers.Number)

顺便说一下,NumPy将生成低数字的单独实例。

其实我不知道这个问题的答案。我认为理论上可以使用ctypes来调用PyNumber_Check,但即使是这个函数也存在争议,而且它肯定是不可移植的。我们现在只需要对测试的内容不那么挑剔。

最后,这个问题源于Python最初没有一个类型树的谓词,如Scheme的数字?,或者Haskell的类型类Num. is检查对象的身份,而不是值是否相等。PHP也有丰富多彩的历史,在PHP5中===显然只对对象起作用,而PHP4中没有。这就是跨语言(包括一种语言的不同版本)的成长之痛。

还有一个问题,在现有的任何答案中都没有指出。Python允许合并任何两个不可变的值,并且预先创建的小int值并不是发生这种情况的唯一方式。Python实现从来不保证这样做,但它们都不仅仅是对小int型进行这样做。


首先,还有一些其他预先创建的值,比如空元组、str和bytes,以及一些短字符串(在CPython 3.6中,它是256个单字符Latin-1字符串)。例如:

>>> a = ()
>>> b = ()
>>> a is b
True

但是,即使非预先创建的值也可以是相同的。考虑以下例子:

>>> c = 257
>>> d = 257
>>> c is d
False
>>> e, f = 258, 258
>>> e is f
True

这并不局限于int值:

>>> g, h = 42.23e100, 42.23e100
>>> g is h
True

显然,CPython没有为42.23e100提供预先创建的浮点值。那么,这里发生了什么?

CPython编译器会在同一个编译单元中合并一些已知不可变类型的常量值,比如int、float、str、bytes。对于一个模块,整个模块是一个编译单元,但在交互式解释器中,每条语句都是一个单独的编译单元。因为c和d是在单独的语句中定义的,所以它们的值不会合并。由于e和f在同一个语句中定义,它们的值被合并。


您可以通过分解字节码来了解发生了什么。试着定义一个函数执行e, f = 128, 128然后调用dis。dis,你会看到只有一个常量值(128,128)

>>> def f(): i, j = 258, 258
>>> dis.dis(f)
  1           0 LOAD_CONST               2 ((128, 128))
              2 UNPACK_SEQUENCE          2
              4 STORE_FAST               0 (i)
              6 STORE_FAST               1 (j)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
>>> f.__code__.co_consts
(None, 128, (128, 128))
>>> id(f.__code__.co_consts[1], f.__code__.co_consts[2][0], f.__code__.co_consts[2][1])
4305296480, 4305296480, 4305296480

你可能会注意到编译器将128作为一个常量存储,尽管字节码实际上并没有使用它,这让你知道CPython编译器所做的优化是多么少。这意味着(非空的)元组实际上不会被合并:

>>> k, l = (1, 2), (1, 2)
>>> k is l
False

把它放到一个函数中,dis它,然后看看co_consts -有一个1和一个2,两个(1,2)元组共享相同的1和2,但不相同,还有一个((1,2),(1,2))元组,它有两个不同的相等元组。


CPython还做了一项优化:字符串实习。与编译器常量折叠不同,这并不局限于源代码字面量:

>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True

另一方面,它仅限于str类型,以及内部存储类型为"ascii compact"、"compact"或"legacy ready"的字符串,并且在许多情况下只有"ascii compact"会被捕获。


无论如何,关于什么值必须是、可能是或不能是不同的规则在不同的实现之间、在相同实现的版本之间、甚至在相同实现的同一副本上运行相同代码之间都是不同的。

为了好玩,学习特定Python的规则是值得的。但是在代码中依赖它们是不值得的。唯一安全的规则是:

不要编写假定两个相同但单独创建的不可变值相同的代码(不要使用x is y,使用x == y) 不要编写假定两个相同但分别创建的不可变值是不同的代码(不要使用x不是y,使用x != y)

或者,换句话说,唯一的用途是测试文档化的单例对象(如None)或只在代码中的一个地方创建的单例对象(如_sentinel = object()习惯用法)。

对于不可变值对象,比如int、字符串或datetimes,对象标识并不是特别有用。最好还是考虑一下平等。标识本质上是值对象的实现细节——因为它们是不可变的,所以对同一个对象的多次引用和对多个对象的多次引用之间没有有效的区别。