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


我认为你的假设是正确的。实验id(对象的身份):

In [1]: id(255)
Out[1]: 146349024

In [2]: id(255)
Out[2]: 146349024

In [3]: id(257)
Out[3]: 146802752

In [4]: id(257)
Out[4]: 148993740

In [5]: a=255

In [6]: b=255

In [7]: c=257

In [8]: d=257

In [9]: id(a), id(b), id(c), id(d)
Out[9]: (146349024, 146349024, 146783024, 146804020)

似乎数字<= 255被视为字面量,以上任何数字都被区别对待!


看看这个:

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

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

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


这取决于你要看的是两个物体是否相等,还是同一个物体。

是检查它们是否是相同的对象,而不仅仅是相等。较小的int可能指向相同的内存位置以提高空间效率

In [29]: a = 3
In [30]: b = 3
In [31]: id(a)
Out[31]: 500729144
In [32]: id(b)
Out[32]: 500729144

您应该使用==来比较任意对象的相等性。你可以用__eq__和__ne__属性来指定行为。


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

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


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


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的“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和is not测试对象的同一性:x是y是真 当且仅当x和y是同一个对象。X不是y会得到 逆真值。

所以下面两个是等价的。

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

从文档中可以看到:

id 返回一个对象的“标识”。这是一个整数(或长 Integer),保证该对象的唯一性和常量 在它的生命周期内。两个生命周期不重叠的对象可能 具有相同的id()值。

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

那么用例是什么呢?PEP8描述:

与像None这样的单例对象的比较应该总是使用is或 不是,不是相等运算符。

这个问题

你问并陈述以下问题(带代码):

为什么下面的代码在Python中会出现意外的行为? >>> a = 256 >>> b = 256 >>> a是b #这是一个预期的结果

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

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

>>> a = 257 >>> b = 257 >>> a是b 这里发生了什么?为什么这是假的?

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

>>> 257等于257 True #但文字数字比较正确

好吧,这看起来像是你的特定Python实现试图变得聪明,除非必要,否则不会在内存中创建冗余值的整数。您似乎表明您正在使用Python的参考实现,即CPython。对CPython很好。

如果CPython能在全局范围内做到这一点,如果它能做到这一点(因为查找会有成本),那就更好了,也许另一个实现可以做到。

但至于对代码的影响,您不应该关心一个整数是否是一个整数的特定实例。您应该只关心该实例的值是什么,并且您可以使用普通的比较操作符,即==。

它做什么

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

>>> a is b

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

为什么我们要用is呢?

This can be a very fast check relative to say, checking if two very long strings are equal in value. But since it applies to the uniqueness of the object, we thus have limited use-cases for it. In fact, we mostly want to use it to check for None, which is a singleton (a sole instance existing in one place in memory). We might create other singletons if there is potential to conflate them, which we might check with is, but these are relatively rare. Here's an example (will work in Python 2 and 3) e.g.

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被无参数调用,什么时候它被无参数调用。这些是is的主要用例——不要用它来测试整数、字符串、元组或其他类似的东西是否相等。


它也发生在字符串上:

>>> s = b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)

现在一切似乎都好了。

>>> s = 'somestr'
>>> b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)

这也是意料之中的。

>>> s1 = b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, True, 4555308080, 4555308080)

>>> s1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, False, 4555308176, 4555308272)

这是意料之外的。


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


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。


还有一个问题,在现有的任何答案中都没有指出。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()习惯用法)。


Python 3.8新增功能:Python行为的变化:

编译器现在在标识检查(is和 Is not)用于某些类型的字面量(例如字符串,int)。 在CPython中,这些通常可以意外地工作,但不能保证 该警告建议用户使用相等性测试(== 和!=)代替。