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

时间:2008-11-20 18:21:17

标签: python int comparison operators identity

为什么以下在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 个答案:

答案 0 :(得分:345)

看看这个:

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

编辑:这是我在Python 2文档中找到的"Plain Integer Objects"Python 3也是如此):

  

当前的实施保持了   所有的整数对象数组   -5到256之间的整数,当你   在你的范围内创建一个int   实际上只是回来参考   现有的对象。所以它应该是   可以改变1的值   怀疑Python的行为   这种情况是未定义的。 : - )

答案 1 :(得分:91)

  

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做了什么?它是一个比较运算符。来自documentation

  

运算符isis not测试对象标识:x is y为真   当且仅当x和y是同一个对象时。 x is not y产生了   反向真值。

以下是相同的。

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

来自documentation

  

<强> id   返回对象的“标识”。这是一个整数(或长整数)   整数)保证对于该对象是唯一的和常量的   在其一生中。两个具有非重叠寿命的对象可以   具有相同的id()值。

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

那么is的用例是什么? PEP8 describes

  

None等单身人士的比较应始终使用is或。{   is not,绝不是平等操作符。

问题

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

  

为什么Python中会出现以下异常情况?

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

预期结果。为什么会这样?它仅表示256a引用的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的主要用例 - 执行使用它来测试整数,字符串,元组或其他类似内容的相等性。

答案 2 :(得分:56)

这取决于您是否想要查看两件事情是否相同,或者是同一个对象。

is检查它们是否是同一个对象,而不仅仅是相同的。小的int可能指向空间效率的相同内存位置

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

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

答案 3 :(得分:40)

我迟到了,你想要一些消息来源吗? *

关于CPython的好处是你实际上可以看到它的来源。我现在要使用3.5版本的链接;找到相应的2.x是微不足道的。

在CPython中,处理创建新C-API对象的int函数是PyLong_FromLong(long v)。该功能的描述是:

  

当前实现为-5到256之间的所有整数保留一个整数对象数组,当您在该范围内创建一个int时,实际上只返回对现有对象的引用。因此应该可以更改值1.我怀疑在这种情况下Python的行为是未定义的。 : - )

不知道你,但我看到了这一点,并想:让我们找到那个数组!

如果你没有摆弄实现CPython C代码,你应该,一切都非常有条理和可读。对于我们的情况,我们需要查看Objects/ subdirectorymain source code directory tree

PyLong_FromLong处理long个对象,因此不难推断我们需要查看longobject.c内部。看完后你可能会认为事情很混乱;他们是,但不要担心,我们正在寻找的功能在line 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 master-code-haxxorz 但我们也不傻,我们可以看到CHECK_SMALL_INT(ival);诱惑地偷看我们;我们可以理解它与此有关。 Let's check it out:

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

如果值get_small_int满足条件,那么它是一个调用函数ival的宏:

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

那么NSMALLNEGINTSNSMALLPOSINTS是什么?如果您猜到了宏,那么您什么也得不到,因为这不是一个很难的问题。 Anyway, here they are

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

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

没有其他地方可去但是继续我们的旅程看get_small_int in all its glory(好吧,我们只看它的身体,因为这是有趣的事情):

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看起来很像我们一直在搜索的那个数组......而且,它是! We could've just read the damn documentation and we would've know all along!

/* 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];

所以,是的,这是我们的家伙。如果要在int范围内创建新的[NSMALLNEGINTS, NSMALLPOSINTS),您只需返回对已预先分配的现有对象的引用。

由于引用引用同一个对象,因此直接发出id()或在其上检查is的身份将返回完全相同的内容。

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

During initialization in _PyLong_Init Python很乐意进入for循环,为你做这个:

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

我希望我的解释现在能清楚地表明你C(双关语显然是有意的)。

但是,257是257?怎么了?

这实际上更容易解释,and I have attempted to do so already;这是因为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

* - 我会尝试以更具介意性的方式说出这一点,以便大多数人能够跟进。

答案 4 :(得分:36)

正如您可以检查source file intobject.c一样,Python会缓存小整数以提高效率。每次创建对小整数的引用时,都指的是缓存的小整数,而不是新对象。 257不是一个小整数,因此它被计算为一个不同的对象。

最好为此目的使用==

答案 5 :(得分:18)

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

答案 6 :(得分:12)

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

答案 7 :(得分:8)

is 身份相等运算符(功能类似于id(a) == id(b));只是两个相等的数字不一定是同一个对象。出于性能原因,一些小整数碰巧是memoized所以它们往往是相同的(这可以做到,因为它们是不可变的)。

另一方面,

PHP's ===运算符被描述为检查相等性并按照Paulo Freitas的评论键入x == y and type(x) == type(y)。这对于常见数字就足够了,但对于以荒谬方式定义is的类,__eq__不同:

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,但即使是那个函数has been debated,它肯定不是可移植的。我们必须对我们现在测试的内容不那么特别。

最后,这个问题源于Python最初没有包含Scheme's number?Haskell's type class Num等谓词的类型树。 is检查对象标识,而不是值相等。 PHP也有丰富多彩的历史记录,其中===仅在对象in PHP5, but not PHP4上表现为is。跨越语言(包括版本的语言)越来越痛苦。

答案 8 :(得分:5)

在现有的任何答案中都没有指出另一个问题。允许Python合并任意两个不可变值,并且预先创建的小int值不是这种情况发生的唯一方式。 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没有为float预先创建的42.23e100值。那么,这里发生了什么?

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还有一个优化:string interning。与编译器常量折叠不同,这不仅限于源代码文字:

>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True

另一方面,它仅限于str类型和internal storage kind "ascii compact", "compact", or "legacy ready"的字符串,并且在很多情况下只有“ascii compact”会被实习。

无论如何,对于什么值必须是,可能是或不可能是不同的规则,从实现到实现,以及相同实现的版本之间,甚至可能在相同副本的相同代码之间运行。同样的实施。

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

  • 不要编写假设两个相等但单独创建的不可变值相同的代码。
  • 不要编写假设两个相等但单独创建的不可变值是不同的代码。

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

答案 9 :(得分:4)

字符串也是如此:

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

现在出乎意料。

答案 10 :(得分:2)

What’s New In Python 3.8: Changes in Python behavior

  

现在,在进行身份检查(is和   is not)用于某些类型的文字(例如字符串,整数)。   这些通常在CPython中偶然地起作用,但不能保证   语言规范。该警告建议用户使用相等性测试(==   和!=)。