is" 运算符在整数中表现异常
为什么下面的代码在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 个回答
我来晚了,不过你想要一些源代码来支持你的答案吗? 我会尽量用简单的方式来表达,这样更多的人可以理解。
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)
那么NSMALLNEGINTS
和NSMALLPOSINTS
是什么呢?它们是宏!在这里可以找到:
#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
。
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
用于测试对象的身份: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
)的比较应该始终使用is
或is not
,而不是使用相等运算符。
问题
你问了以下问题(附带代码):
为什么以下代码在 Python 中表现得很奇怪?
>>> a = 256 >>> b = 256 >>> a is b True # This is an expected result
这不是一个预期的结果。为什么会这样?这仅仅意味着两个值为 256
的整数被 a
和 b
引用的是同一个整数实例。整数在 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
的主要使用场景——不要 用它来测试整数、字符串、元组或其他类似的东西的相等性。