“是”运算符的意外行为与整数

为什么在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运算符可以区分这种差异。 为什么有漏洞的抽象? 当我不知道数字是否是数字时,比较两个任意对象是否相同是一个更好的方法?

看看这个:

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

编辑:这是我在Python 2文档, “纯整型对象” (这是相同的Python 3 )中find:

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

这取决于你是否希望看到两件事情是平等的,还是同一个对象。

is检查,看看他们是否是相同的对象,不只是平等的。 小的整数可能指向相同的存储空间效率

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

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

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 nottesting对象的身份:当且仅当x和y是同一个对象时, x is y是真的。 x is not y产生逆真值。

所以下面是相同的。

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

从文档 :

id返回一个对象的“身份”。 这是一个整数(或长整数),在整个生命周期中保证它是唯一的,并且是常量。 两个具有非重叠生命周期的对象可能具有相同的id()值。

请注意,CPython(Python的参考实现)中的对象的id是内存中的位置的事实是实现细节。 Python的其他实现(例如Jython或IronPython)可以很容易地为id实现不同的实现。

那么,这个用例is什么呢? PEP8描述 :

比较像None这样的单身人士应该总是用“ is或“ is not ,而不是“平等”。

问题

你问,并说,以下问题(与代码):

为什么在Python中以下行为意外?

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

不是预期的结果。 为什么会这样呢? 它只意味着由ab引用的值为256的整数是整数的同一个实例。 整数在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的特定实现正试图变得聪明,而不是在内存中创build冗余值整数,除非必须。 您似乎表示您正在使用Python的参考实现,即CPython。 适合CPython。

如果CPython可以在全球范围内做到这一点,如果它可以做得这么便宜(因为在查找中会花费一些代价),也许还会有另一种实现。

但是至于对代码的影响,你不应该在乎一个整数是一个整数的特定实例。 你应该只关心那个实例的值是什么,并且你会使用普通的比较运算符,即==

什么is

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

 >>> a is b 

是相同的

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

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

相对来说,这可以是一个非常快速的检查,检查两个很长的string是否相等。 但是由于它适用于对象的唯一性,因此我们对它的使用情况有限。 实际上,我们主要是想用它来检查None ,它是一个单例(存储在一个地方的唯一实例)。 如果有可能混淆它们,我们可能会创build其他单例,我们可能会检查这些单例,但这些比较less见。 这是一个例子(将在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和sentinel,我们可以区分bar何时被调用而没有参数,什么时候被调用。 这些是主要的用例 – 不要用它来testing整数,string,元组或其他类似的东西。

正如你可以检查源文件intobject.c ,Pythoncaching小整数效率。 每当你创build一个小整数的引用时,你引用caching的小整数,而不是一个新的对象。 257不是一个小整数,所以它被计算为一个不同的对象。

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

我迟到了,但是,你想要一些你的答案来源? *

关于CPython的好处是,你可以真正的看到源代码。 我现在要使用3.5版本的链接; find相应的2.x是微不足道的。

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

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

不知道你,但我看到这一点,并认为: 让我们find这个arrays!

如果你还没有弄清楚实现CPython的C代码, 你应该一切都非常有组织和可读性。 对于我们的情况,我们需要查看主源代码目录树的Objects/子目录 。

PyLong_FromLong处理long对象,所以我们不应该很难推断出我们需要在longobject.c里面longobject.c 。 看到里面以后,你会觉得事情是混乱的; 他们是,但不要害怕,我们正在寻找的function令人心寒,在line 230等待我们检查出来。 这是一个小function,所以主体(不包括声明)很容易粘贴在这里:

 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 主代码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的macros:

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

那么NSMALLNEGINTSNSMALLPOSINTS什么? 如果你猜macros,你什么也得不到,因为这不是一个很难的问题。 无论如何,在这里他们是

 #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)范围内创build一个新的int[NSMALLNEGINTS, NSMALLPOSINTS)你只需返回一个已经预先分配[NSMALLNEGINTS, NSMALLPOSINTS)对象的引用。

由于引用是指同一个对象,因此直接发送id()或者检查身份是否会返回完全相同的内容。

但是,他们什么时候分配?

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

 for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++, v++) { // Look me up! } 

我希望我的解释现在已经清楚地明白了C (双关语)的含义。


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

这实际上更容易解释, 我已经试图这样做了 ; 这是因为Python会执行这个交互式语句:

 >>> 257 is 257 

作为一个单一的块。 在这个语句的编译过程中,CPython会看到你有两个匹配的文字,并使用相同的代表257 PyLongObject 。 你可以看到这个,如果你自己编译并检查它的内容:

 >>> 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) 

那么将返回True


* – 我会试着用更简洁的方式来说明这一点,以便大部分人能够跟随。

我认为你的假设是正确的。 用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被视为文字,上面的任何东西都被区别对待!

对于不可变的值对象,如整数,string或date时间,对象标识不是特别有用。 考虑平等最好。 Identity本质上是一个值对象的实现细节 – 因为它们是不可变的,所以对同一个对象或多个对象进行多重引用没有有效的区别。

is身份相等运算符(function像id(a) == id(b) ); 只是两个相同的数字不一定是同一个对象。 由于性能的原因,一些小的整数碰巧被记忆,所以它们往往是相同的(这可以做,因为它们是不可改变的)。

另一方面, PHP的 ===操作符被描述为检查相等性并按照Paulo Freitas的注释键入: x == y and type(x) == type(y) 。 这对于常见的数字就足够了,但是对于以荒谬的方式定义__eq__类是不同的:

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

PHP显然允许“内置”类(我认为是在C级而不是在PHP中)实现相同的事情。 一个稍微荒谬的用法可能是一个计时器对象,每当它被用作一个数字时,该对象具有不同的值。 相当你为什么要模拟Visual Basic的Now而不是显示它是一个评估与time.time()我不知道。

Greg Hewgill(OP)作了一个澄清的评论:“我的目标是比较对象的身份,而不是价值的平等,除了数字,我想把对象的身份看作是价值的平等。

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

我们可以尝试使用我们所知道的所有数字types,但是这将不可避免地是不完整的。 types模块包含一个StringTypes列表,但不包含NumberTypes。 自Python 2.6以来,内置的数字类有一个基类数字。数字,但它有相同的问题:

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

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

我实际上并不知道这个问题的这个变种的答案。 我想在理论上可以使用ctypes来调用PyNumber_Check ,但是即使这个函数已经被讨论了 ,而且肯定不是可移植的。 我们只需要对我们现在testing的东西不那么特别。

最后,这个问题起源于Python,它最初没有一个types树,其谓词像Scheme的 number? ,或Haskell的 types类 Num 。 is检查对象标识,而不是值相等。 PHP也有丰富多彩的历史,其中===显然只在PHP5中的对象上,而不是在PHP4中 。 跨越语言(包括版本之一)正在成长的痛苦。

它也发生在string:

 >>> 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) 

现在这是意想不到的。

看看这里

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