首页 文章

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

提问于
浏览
409

为什么以下在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在内部实现,使得"small"整数以不同于大整数的方式存储, is 运算符可以区分 . 为什么泄漏抽象?当我不知道它们是否是数字时,比较两个任意对象以查看它们是否相同的更好的方法是什么?

11 回答

  • 72

    看看here

    当前实现为-5到256之间的所有整数保留一个整数对象数组,当您在该范围内创建一个int时,实际上只返回对现有对象的引用 .

  • 2

    在任何现有的答案中都指出了's another issue that isn't . 允许Python合并任意两个不可变值,并且预先创建的小int值不是这种情况发生的唯一方式 . Python实现永远不能保证这样做,但是他们所做的不仅仅是小的int .


    首先,还有一些其他预先创建的值,例如空 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.23e100 预先创建的 float 值 . 那么,这里发生了什么?

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


    您可以通过反汇编字节码来查看正在发生的事情 . 尝试定义一个函数 e, f = 128, 128 ,然后在其上调用 dis.dis ,并且'll see that there'是一个常量值 (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 存储为常量,即使它是's not actually used by the bytecode, which gives you an idea of how little optimization CPython'的编译器也是如此 . 这意味着(非空)元组实际上不会最终合并:

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

    把它放在一个函数中, dis ,看看 co_consts -there是一个 1 和一个 2 ,两个 (1, 2) 元组共享相同的 12 但是不相同,而 ((1, 2), (1, 2)) 元组有两个截然不同的元组 .


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

    >>> 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() 成语) .

  • 12

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

    为此目的最好使用 == .

  • 4

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

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

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

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

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

    如果你没有摆弄实现CPython的 C 代码,那么一切都非常有条理和可读 . 对于我们的情况,我们需要查看main source code directory treeObjects/ subdirectory .

    PyLong_FromLong 处理 long 对象因此不难推断我们需要查看longobject.c内部 . 看完后你可能会认为事情很混乱;他们是,但不要担心,我们正在寻找的功能令人不寒而栗,等待我们检查出来 . 这是一个小功能,所以主体(不包括声明)很容易粘贴在这里:

    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; 
    }
    

    现在,我们不是主人 - 代码 - 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)
    

    所以它是一个宏,如果值 ival 满足条件,则调用函数 get_small_int

    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(好吧,我们'll just look at it'的身体,因为这是有趣的事情):

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

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

    由于引用引用同一个对象,直接发出 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 .


      • 我会尝试用更多的介绍方式来说明这一点,以便大多数人能够遵循 .
  • 317

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

  • 33

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

  • 18

    is 是身份相等运算符(功能类似于 id(a) == id(b) );它's just that two equal numbers aren' t必然是同一个对象 . 出于性能原因,一些小整数碰巧是memoized所以它们往往是相同的(这可以做到,因为它们是不可变的) .

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

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

    PHP显然允许"built-in"类相同的东西(我认为它意味着在C级实现,而不是在PHP中实现) . 稍微不那么荒谬的用法可能是一个计时器对象,每次's used as a number. Quite why you'想要模拟Visual Basic的 Now 而不是显示它是 time.time() 我不知道的评估时,它具有不同的值 .

    Greg Hewgill(OP)做了一个澄清评论“我的目标是比较对象身份,而不是 Value 的平等 . 除了数字,我想把对象身份看作是 Value 平等 . ”

    这将有另一个答案,因为我们必须将事物分类为数字,以选择我们是否与 ==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 . 这是跨越语言(包括版本的一种)的不断增长的痛苦 .

  • 3

    它也适用于字符串:

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

    现在这出乎意料 .

  • 55

    看看这个:

    >>> 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的行为是未定义的 . :-)

  • 36

    Python的“is”运算符会出现意外的整数行为吗?

    总之 - 让我强调: Do not use is to compare integers.

    这不是你应该有任何期望的行为 .

    相反,使用 ==!= 分别比较相等性和不等式 . 例如:

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

    运算符是和不是对象标识的测试:当且仅当x和y是同一个对象时,x是y . x不是y产生反向真值 .

    所以以下是等价的 .

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

    来自documentation

    id返回对象的“标识” . 这是一个整数(或长整数),保证在该生命周期内该对象是唯一且恒定的 . 具有非重叠生存期的两个对象可以具有相同的id()值 .

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

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

    像None这样的单例的比较应该总是使用is或者不是,而不是相等的运算符 .

    问题

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

    为什么以下在Python中出现意外行为? >>> a = 256

    b = 256
    a是b
    真#这是预期的结果

    这不是预期的结果 . 为什么会这样?它只表示 ab 引用的 256 值的整数是整数的相同实例 . 整数在Python中是不可变的,因此它们无法改变 . 这应该对任何代码都没有影响 . 不应该这样 . 它只是一个实现细节 .

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

    a = 257
    b = 257
    a是b
    假#这里发生了什么?为什么这是假的?

    看起来我们现在有两个独立的整数实例,其内存值为 257 . 由于整数是不可变的,这会浪费内存 . 让's hope we'不要浪费太多 . 我们可能不是 . 但这种行为并不能保证 .

    257是257
    真实#然而文字数字正确比较

    好吧,这看起来像你的Python的特定实现是试图聪明,而不是在内存中创建冗余值整数,除非它必须 . 您似乎表明您正在使用Python的引用实现,即CPython . 适合CPython .

    如果CPython可以在全球范围内实现这一目标可能会更好,如果它可以这么便宜(因为在查找中会有成本),或许可能是另一种实现 .

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

    什么是

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

    >>> a is b
    

    是相同的

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

    为什么我们想要使用呢?

    这可以是一个非常快速的检查,相比之下,检查两个非常长的字符串是否相等 . 但由于它适用于对象的唯一性,因此我们对它的使用情况有限 . 事实上,我们主要想用它来检查 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 的主要用例 - 不要用它来测试整数,字符串,元组或其他类似的东西的相等性 .

  • 8

    这取决于你是否想要看两件事是否相同,或者是同一个对象 .

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

相关问题