"is"运算符在处理整数时表现出意外的行为

617

为什么以下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个回答

457

看看这个:

>>> a = 256
>>> b = 256
>>> id(a) == id(b)
True
>>> a = 257
>>> b = 257
>>> id(a) == id(b)
False

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

当前实现为所有介于-5256之间的整数保留一个整数对象的数组。当您创建该范围内的整数时,实际上只会返回对现有对象的引用。

因此,整数256是相同的,但257不是。这是CPython的实现细节,并不能保证其他Python实现也一样。


160

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 not 运算符用来测试对象的身份:当且仅当 x 和 y 引用同一个对象时,x is y 返回 true。否则,x is not y 返回 false。

因此,下面两种写法等价。

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

根据文档

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

请注意,CPython(Python的参考实现)中对象的id是内存中的位置是一种实现细节。 Python的其他实现(例如Jython或IronPython)可以轻松地对id进行不同的实现。

那么is的用例是什么? PEP8描述

应始终使用isis not比较单例(如None),而不是等式运算符。

问题

您提出并陈述了以下问题(带有代码):

Why does the following behave unexpectedly in 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的特定实现,它试图变得聪明,除非必须要这样做,否则不会在内存中创建重复的值。你似乎表明你正在使用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和一个哨兵(sentinel),我们能够区分在调用bar时是否传递了参数。这些是is的主要用例 - 不要用它来测试整数、字符串、元组或其他类似东西的相等性。


这些是 is 的主要用例 - 不要使用它来测试整数、字符串、元组或其他类似的东西的相等性。然而,我正在尝试将一个简单的状态机集成到我的类中,由于状态是不透明的值,其唯一可观察的属性是相同或不同,因此使用 is 进行比较看起来非常自然。我计划使用已经 interned 的字符串作为状态。我本来更喜欢使用普通整数,但不幸的是 Python 不能 intern 整数(0 is 0 是一个实现细节)。 - Alexey
2
@Alexey听起来你需要枚举?https://dev59.com/FVoU5IYBdhLWcg3wTVse - Russia Must Remove Putin
另外一个你需要使用 is 的情况是处理浮点数 NaN:math.nan==math.nan 总是返回 False - Mark Ransom
2
@MarkRansom 不要在float中使用is,即使你正在检查NaN:float('nan') is float('nan')返回False,原因与int('257') is int('257')相同:它们是不同的实例,尽管它们是无法区分的。 - Jasmijn
@Jasmijn 我曾经认为Python中的NaN是一个单例对象,但显然我错了。检查NaN的更好方法是如何检查NaN值? - Mark Ransom
显示剩余5条评论

78

我来晚了,但你需要一些源代码来解答吗? 我会尝试用简单易懂的方式表达,以便更多人能够理解。


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)

那么什么是NSMALLNEGINTSNSMALLPOSINTS?它们是宏!这里是它们:

#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循环来完成这个过程:

for (ival = -NSMALLNEGINTS; ival <  NSMALLPOSINTS; ival++, v++) {

查看源代码以阅读循环体!

我希望我的解释现在让你更清楚地了解了C语言(明显是双关语)。


但是,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


63

这要看你是想检查两个东西是否相等,还是同一个对象。

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__属性来指定行为。


39

如您可以在源文件intobject.c中查看,Python 为了提高效率而缓存了小整数。每次您创建一个对小整数的引用时,您都是在引用缓存的小整数,而不是一个新对象。257并非小整数,所以它被计算为一个不同的对象。

为此,最好使用==


22

我认为你的假设是正确的。尝试使用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被视为字面量,而大于此范围的数字则会被区别对待!


2
这是因为在启动时创建了表示从-5到+256的值的对象 - 因此所有使用这些值的用法都使用预构建的对象。几乎所有引用该范围之外的整数都会在每次引用时创建一个新的内部对象。我认为术语“字面量”的使用很令人困惑 - 字面量通常指任何在代码中键入的值 - 因此源代码中的所有数字都是字面量。 - Tony Suffolk 66

17

现有的答案中没有指出另一个问题。Python可以合并任意两个不可变值,并且预创建的小整数值并不是唯一的途径。Python实现不能保证这样做,但它们都会对不止小整数进行操作。


其次,还有一些其他预创建的值,例如空的tuplestrbytes,以及一些短字符串(在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.23e100float值。那么,这里发生了什么?

CPython编译器将一些已知不可变类型的常量值(如intfloatstrbytes)合并在同一编译单元中。对于模块来说,整个模块是一个编译单元,但在交互式解释器中,每个语句都是一个单独的编译单元。由于cd在不同的语句中定义,它们的值没有被合并。由于ef在同一语句中定义,它们的值被合并了。


您可以通过反汇编字节码来查看结果。尝试定义一个执行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)元组共享相同的12但不是相同的,还有一个((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 is not y,使用x != y

换句话说,仅在测试文档化的单例(例如None)或仅在代码中的一个位置创建的对象(例如_sentinel = object()习语)时使用is


1
更易懂的建议是:不要使用 x is y 进行比较,而应该使用 x == y。同样地,不要使用 x is not y,而应该使用 x != y - smci
看着这个问题,为什么在一行中写a=257; b=257时,a is b是True。 - Joe

15
对于不可变值对象,如整数、字符串或日期时间,对象身份并不特别有用。更好的方式是考虑相等性。对于值对象,身份本质上是实现细节——因为它们是不可变的,拥有多个引用指向同一对象或多个对象之间没有有效区别。

9

is是身份相等运算符(类似于id(a) == id(b));只是两个相等的数字不一定是同一个对象。出于性能原因,一些小整数被记忆化,因此它们往往是相同的(由于它们是不可变的)。

PHP===运算符则被描述为检查相等和类型:x == y and type(x) == type(y),这对于常见的数字足够了,但对于定义了荒谬的__eq__的类来说,与is不同:

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

PHP 显然允许"内置"类做同样的事情(我认为这意味着在C级别实现,而不是在PHP中)。一个稍微不那么荒谬的用途可能是计时器对象,每次用作数字时都有不同的值。为什么你想模拟Visual Basic的Now而不是展示它是一个使用time.time()的评估,我不知道。
Greg Hewgill(OP)发表了一条澄清性评论:“我的目标是比较对象身份,而不是值的相等性。除了数字之外,在数字方面,我希望将对象身份视为值的相等性。”
这会有另一个答案,因为我们必须将事物分类为数字或非数字,以选择是否使用 == is 进行比较。CPython定义了number protocol,包括PyNumber_Check,但这在Python本身中不可访问。
我们可以尝试使用isinstance与我们所知道的所有数字类型,但这必然是不完整的。types模块包含了StringTypes列表,但没有NumberTypes。自Python 2.6以来,内置的数字类具有一个基类numbers.Number,但它也有同样的问题:
import numpy, numbers
assert not issubclass(numpy.int16,numbers.Number)
assert issubclass(int,numbers.Number)

顺便说一下,NumPy 会产生低数字的独立实例。
我并不知道这个问题的答案。我想理论上可以使用 ctypes 调用 PyNumber_Check,但即使这个函数也存在争议,而且肯定不是可移植的。现在我们只需要对我们测试的内容更加宽泛。
最后,这个问题源于Python最初没有像Schemenumber?或者Haskelltype class Num那样带有谓词的类型树。 is检查对象身份,而不是值相等。PHP也有一个丰富多彩的历史,在PHP5中只对对象进行比较,但在PHP4中不是这样===显然与is相同。这就是跨语言(包括同一版本的语言)移植的成长烦恼。

5

字符串也会发生这种情况:

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

现在这真是出乎意料。

偶然发现这个问题 - 同意,甚至更奇怪。所以我试着解决它,但它变得更加奇怪了 - 与空格有关。例如,字符串'xx''xxx'都是预期的结果,但是'x x'却不是。 - Brian
2
这是因为如果字符串中没有空格,它看起来就像一个符号。名称自动内部化,因此,如果在 Python 会话的任何地方有命名为 xx 的内容,则该字符串已经被内部化;并且如果它只是类似名称的启发式方法也可能被使用。与数字一样,这是可以做到的,因为它们是不可变的。 https://docs.python.org/2/library/functions.html#intern http://guilload.com/python-string-interning/ - Yann Vernier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接