“是”运算符的意外行为与整数
为什么在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
做什么的? 这是一个比较运算符。 从文档 :
运算符
is
,is not
testing对象的身份:当且仅当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
这不是预期的结果。 为什么会这样呢? 它只意味着由a
和b
引用的值为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)
那么NSMALLNEGINTS
和NSMALLPOSINTS
什么? 如果你猜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时,你实际上只是返回一个对现有对象的引用。