关于python:“is”运算符与整数意外行为

“is” operator behaves unexpectedly with integers

为什么下面的行为在Python中出乎意料?

1
2
3
4
5
6
7
8
9
10
>>> 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时,Python2.3.3显示了上述99到100之间的行为。

基于以上,我可以假设python是在内部实现的,这样"小"整数以不同于大整数的方式存储,并且is操作符可以分辨出不同之处。为什么是漏抽象?当我事先不知道两个任意对象是否是数字时,比较两个任意对象是否相同的更好方法是什么?


看看这个:

1
2
3
4
5
6
7
8
9
10
11
12
>>> a = 256
>>> b = 256
>>> id(a)
9987148
>>> id(b)
9987148
>>> a = 257
>>> b = 257
>>> id(a)
11662816
>>> id(b)
11662828

编辑:下面是我在python 2文档中找到的,"纯整数对象"(对于python 3来说是一样的):

The current implementation keeps an
array of integer objects for all
integers between -5 and 256, when you
create an int in that range you
actually just get back a reference to
the existing object. So it should be
possible to change the value of 1. I
suspect the behaviour of Python in
this case is undefined. :-)


Python's"is" operator behaves unexpectedly with integers?

总之,我要强调的是:不要使用is来比较整数。

这不是你应该期待的行为。

相反,使用==!=分别比较平等和不平等。例如:

1
2
3
4
5
6
7
>>> 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是做什么的?它是一个比较运算符。从文档中:

The operators is and is not test for object identity: x is y is true
if and only if x and y are the same object. x is not y yields the
inverse truth value.

所以下面是等价的。

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

从文档中:

id
Return the"identity" of an object. This is an integer (or long
integer) which is guaranteed to be unique and constant for this object
during its lifetime. Two objects with non-overlapping lifetimes may
have the same id() value.

注意,cpython(Python的引用实现)中的对象ID是内存中的位置,这是一个实现细节。其他的python实现(如jython或ironpython)很容易对id有不同的实现。

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

Comparisons to singletons like None should always be done with is or
is not, never the equality operators.

问题

您可以询问并说明以下问题(带代码):

Why does the following behave unexpectedly in Python?

1
2
3
4
>>> a = 256
>>> b = 256
>>> a is b
True           # This is an expected result

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

但也许我们应该高兴的是,每当我们声明一个等于256的值时,内存中就没有一个新的独立实例。

1
2
3
4
>>> a = 257
>>> b = 257
>>> a is b
False          # What happened here? Why is this False?

看起来我们现在在内存中有两个值为257的整数实例。因为整数是不可变的,所以这会浪费内存。希望我们不要浪费太多。我们可能不是。但这种行为并不能保证。

1
2
>>> 257 is 257
True           # Yet the literal numbers compare properly

好吧,这看起来像是您的特定的python实现试图变得智能,除非必须这样做,否则不要在内存中创建冗余值的整数。您似乎表明您正在使用python的引用实现,即cpython。对CPython有好处。

如果cpython能够在全球范围内做到这一点,如果它能够以如此低的成本做到这一点(就像查找过程中的成本一样),也许另一个实现可能会更好。

但是对于代码的影响,您不应该关心整数是否是整数的特定实例。您应该只关心该实例的值是什么,并为此使用常规的比较运算符,即==

is的作用

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

1
>>> a is b

是一样的

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

那我们为什么要用is

这可以是一个非常快速的检查,例如,检查两个非常长的字符串的值是否相等。但由于它适用于对象的唯一性,因此我们对它的用例有限。实际上,我们主要想用它来检查None,这是一个单例(内存中一个地方存在的唯一实例)。如果有可能将它们合并,我们可能会创建其他单例,我们可以与is核对,但这些比较罕见。下面是一个示例(将在python 2和3中使用),例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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()

哪些印刷品:

1
2
3
4
no argument given to foo
no argument given to bar
argument to bar: None
argument to bar: baz

因此,我们看到,使用is和sentinel,我们能够区分何时调用bar,何时不带参数,何时使用None。这些是EDOCX1的主要用例(0)——不要使用它来测试整数、字符串、元组或其他类似的东西是否相等。


这取决于你想看两个东西是相等的还是相同的。

is检查它们是否是同一对象,而不仅仅是同一对象。为了节省空间,小整数可能指向相同的内存位置。

1
2
3
4
5
6
In [29]: a = 3
In [30]: b = 3
In [31]: id(a)
Out[31]: 500729144
In [32]: id(b)
Out[32]: 500729144

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


我迟到了,但你想知道你的答案吗?*

关于cpython的好处是你可以看到它的来源。现在我将使用3.5版本的链接;找到相应的2.x版本是很简单的。

在cpython中,处理创建新int对象的C-API函数是PyLong_FromLong(long v)。此功能的说明如下:

The current implementation keeps an array of integer objects for all integers between -5 and 256, when you create an int in that range you actually just get back a reference to the existing object. So it should be possible to change the value of 1. I suspect the behaviour of Python in this case is undefined. :-)

不知道你的情况,但我看到了,我想:我们去找那个阵列吧!

如果您没有摆弄实现cpython的C代码,那么一切都是非常有组织和可读的。对于我们的例子,我们需要查看主源代码目录树的Objects/子目录。

PyLong_FromLong处理long对象,因此不难推断我们需要窥视longobject.c内部。从内部看,你可能会认为事情是混乱的;它们是,但不要害怕,我们正在寻找的功能是让line 230等着我们检查它。这是一个很小的函数,因此主体(不包括声明)很容易粘贴在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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主代码haxzorz,但我们也不是哑巴,我们可以看到CHECK_SMALL_INT(ival);以诱人的方式偷看我们,我们可以理解这与此有关。让我们来看看:

1
2
3
4
#define CHECK_SMALL_INT(ival) \
    do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \
        return get_small_int((sdigit)ival); \
    } while(0)

因此,如果值ival满足以下条件,它是一个调用函数get_small_int的宏:

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

那么,什么是NSMALLNEGINTSNSMALLPOSINTS?如果你猜到宏,你什么也得不到,因为这不是一个很难的问题。不管怎样,它们是:

1
2
3
4
5
6
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

所以我们的条件是if (-5 <= ival && ival < 257)get_small_int

除了继续我们的旅程,没有其他地方可以去,我们可以从它的辉煌中(好吧,我们只是看看它的身体,因为这是有趣的事情):

1
2
3
4
PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);

好的,声明一个PyObject,断言先前的条件保持并执行分配:

1
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];

small_ints看起来很像我们一直在寻找的那个数组。是的!我们本来可以读读那些该死的文件的,我们早就知道了!以下内容:

1
2
3
4
5
6
/* 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循环,请为您执行以下操作:

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

我希望我的解释使你现在清楚地了解了(双关语显然是有意的)。

但是,257是257吗?怎么了?

这实际上更容易解释,我已经尝试过这样做;这是因为python将执行这个交互式语句:

1
>>> 257 is 257

作为一个整体。在编制本声明的过程中,cpython将看到您有两个匹配的文本,并将使用同一个表示257PyLongObject。如果您自己进行编译并检查其内容,可以看到这一点:

1
2
3
>>> codeObj = compile("257 is 257","blah!","exec")
>>> codeObj.co_consts
(257, None)

当cpython执行操作时,它现在只加载完全相同的对象:

1
2
3
4
5
>>> 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

>--我将尝试以更具介绍性的方式来表达这一点,以便大多数人能够跟随。


正如您可以签入源文件intobject.c一样,python缓存小整数以提高效率。每次创建对小整数的引用时,都会引用缓存的小整数,而不是新对象。257不是一个小整数,因此它是作为另一个对象计算的。

因此最好使用==


我认为你的假设是正确的。用id进行实验(物体的同一性):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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被视为文字,而上面的任何内容都被不同地对待!


对于不可变的值对象,例如整数、字符串或日期时间,对象标识并不是特别有用。最好考虑平等。标识本质上是值对象的实现细节——因为它们是不可变的,所以对同一对象或多个对象具有多个引用之间没有有效的区别。


is是身份平等算子(作用与id(a) == id(b)相似),只是两个相等的数不一定是同一个对象。出于性能原因,一些小整数碰巧被记忆化,因此它们往往是相同的(这可以做到,因为它们是不可变的)。

另一方面,php的===操作符被描述为检查等式和类型:根据paulo freitas的评论,x == y and type(x) == type(y)。这对于普通数字就足够了,但对于以荒谬的方式定义__eq__的类,这与is不同:

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

PHP显然允许"内置"类使用相同的功能(我认为这是指在C级别实现,而不是在PHP中实现)。稍微不那么荒谬的用法可能是计时器对象,它每次用作数字时都有不同的值。我不知道你为什么要模仿VisualBasic的Now,而不是显示它是对time.time()的评估。

格雷格·休吉尔(GregHewgill)做了一个澄清的评论:"我的目标是比较对象的同一性,而不是价值的平等。除了数字之外,我希望在这里将对象标识视为值相等。"

这将是另一个答案,因为我们必须将事物归类为数字,以选择我们是与==还是is进行比较。cpython定义了数字协议,包括pynumber_检查,但不能从python本身进行访问。

我们可以尝试在已知的所有数字类型中使用isinstance,但这不可避免地是不完整的。"类型"模块包含字符串类型列表,但不包含数字类型。自python 2.6以来,内置的数字类有一个基类numbers.Number,但它也有同样的问题:

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

顺便说一下,numpy将生成单独的低数字实例。

我真的不知道这个问题变种的答案。我想理论上可以用ctypes来调用PyNumber_Check,但即使是这个函数也有争议,而且它肯定不可移植。我们只需要对我们现在测试的内容不那么特别。

最后,这个问题源于python最初没有带有诸如scheme的number?或haskell的type class num.is这样的谓词的类型树,而不是值相等。PHP也有丰富的历史,其中===显然只在php5中的对象上表现为is,而不是php4。这就是跨语言(包括一种语言的版本)移动所带来的日益增长的痛苦。


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

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

现在一切都好了。

1
2
3
4
>>> s = 'somestr'
>>> b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)

这也是意料之中的。

1
2
3
4
5
6
7
8
>>> 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)

这是出乎意料的。


还有一个问题没有在任何现有答案中指出。python可以合并任意两个不可变的值,而预先创建的小int值并不是实现这一点的唯一方法。一个Python实现永远不能保证做到这一点,但它们所做的不仅仅是小整数。

一方面,还有一些其他预先创建的值,例如空的tuplestrbytes,以及一些短字符串(在cpython 3.6中,它是256个单字符拉丁-1字符串)。例如:

1
2
3
4
>>> a = ()
>>> b = ()
>>> a is b
True

但是,即使是未预先创建的值也可以相同。考虑这些例子:

1
2
3
4
5
6
7
>>> c = 257
>>> d = 257
>>> c is d
False
>>> e, f = 258, 258
>>> e is f
True

这不仅限于int值:

1
2
3
>>> g, h = 42.23e100, 42.23e100
>>> g is h
True

显然,cpython没有为42.23e100提供预先创建的float值。那么,这是怎么回事?

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

你可以通过分解字节码看到发生了什么。试着定义一个执行e, f = 128, 128的函数,然后在它上面调用dis.dis,你会发现有一个常量值(128, 128)

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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的编译器的优化功能有多小。这意味着(非空)元组实际上不会合并:

1
2
3
>>> k, l = (1, 2), (1, 2)
>>> k is l
False

把它放到一个函数中,dis它,看看co_consts—有一个1和一个2,两个(1, 2)元组共享同一个12但不相同,一个((1, 2), (1, 2))元组有两个不同的相等的元组。

还有一个CPython做的优化:字符串实习生。与编译器常量折叠不同,这不局限于源代码文本:

1
2
3
4
>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True

另一方面,它仅限于str类型,以及内部存储类型为"ascii-compact"、"compact"或"legacy-ready"的字符串,在许多情况下,只有"ascii-compact"会被Internet。

无论如何,对于值必须是、可能是或不能是不同的规则,在不同的实现之间、同一个实现的版本之间、甚至在同一个实现的同一副本上运行同一代码之间,都会有所不同。

为了取乐,学习特定Python的规则是值得的。但是在代码中依赖它们是不值得的。唯一安全的规则是:

  • 不要编写假定两个相等但分别创建的不可变值相同的代码。
  • 不要编写假定两个相等但分别创建的不可变值是不同的代码。

或者,换句话说,只使用is来测试文档中的单例(如None),或者只在代码中的一个位置创建的单例(如_sentinel = object())。


看看这里

The current implementation keeps an array of integer objects for all
integers between -5 and 256, when you create an int in that range you
actually just get back a reference to the existing object.