代码之家  ›  专栏  ›  技术社区  ›  Greg Hewgill

“is”运算符对整数的行为异常

  •  424
  • Greg Hewgill  · 技术社区  · 16 年前

    为什么下面的行为在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时,Python2.3.3显示了上述99到100之间的行为。

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

    11 回复  |  直到 6 年前
        1
  •  330
  •   Ringil    8 年前

    看看这个:

    >>> 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的行为 此情况未定义。-)

        2
  •  81
  •   Aaron Hall    7 年前

    python的_156;is_157; operator对整数的行为异常?

    总之,我要强调: 不要使用 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
    

    解释

    要了解这一点,您需要了解以下内容。

    首先,什么是 怎么办?它是一个比较运算符。从 documentation :

    算子 is not 对象标识测试: x is y 是真的 如果且仅当X和Y是同一对象时。 x is not y 收益率 逆真值。

    所以下面是等价的。

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

    documentation :

    id 返回对象的标识。这是一个整数(或长 整数),保证此对象的唯一性和常量 在它的一生中。生命周期不重叠的两个对象可以 有一样的 id() 价值。

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

    那么用例是用来做什么的呢 ? PEP8 describes :

    与单子比较,如 None 应该总是用 不是 ,而不是相等运算符。

    问题

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

    为什么下面的行为在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能够在全球范围内做到这一点,如果它能够以如此低的成本做到这一点(就像查找过程中的成本一样),也许另一个实现可能会更好。

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

    什么

    检查一下 身份证件 两个物体是相同的。在CPython 身份证件 是内存中的位置,但它可以是另一个实现中的其他唯一标识号。用代码重述这一点:

    >>> a is b
    

    是一样的

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

    我们为什么要用 那么呢?

    这可以是一个非常快速的检查,例如,检查两个非常长的字符串的值是否相等。但由于它适用于对象的唯一性,因此我们对它的用例有限。实际上,我们主要想用它来检查 没有 ,它是一个单例(内存中一个位置上存在的唯一实例)。如果有可能将它们合并,我们可能会创建其他的单例,我们可以检查一下。 但这些都是相对罕见的。下面是一个示例(将在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
    

    所以我们看到, 作为一个哨兵,我们能够区分 bar 调用时不带参数 没有 . 这些是 -做 使用它来测试整数、字符串、元组或其他类似的东西是否相等。

        3
  •  55
  •   vallentin Remi    7 年前

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

    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__ 属性。

        4
  •  37
  •   Dimitris Fasarakis Hilliard    7 年前

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

    关于cpython的好处是你可以看到它的来源。我将使用链接 3.5 暂时释放;找到相应的 2.x 一个是微不足道的。

    在CPython C-API 处理创建新的 int 对象是 PyLong_FromLong(long v) . 此功能的说明如下:

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

    不知道你的情况,但我看到了这一点并认为: 让我们找到那个阵列!

    如果你没有摆弄 C 代码实现cpython 你应该 ,一切都很有条理和可读性。对于我们的案例,我们需要查看 Objects/ subdirectory main 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 主代码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)
    

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

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

    所以我们的情况是 if (-5 <= ival && ival < 257) 呼叫 GETH SimultIn .

    除了继续我们的旅程 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)
    

    所以 将返回 True .


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

        5
  •  36
  •   vallentin Remi    7 年前

    你可以办理登机手续 source file intobject.c ,python缓存小整数以提高效率。每次创建对小整数的引用时,都会引用缓存的小整数,而不是新对象。257不是一个小整数,因此它是作为另一个对象计算的。

    最好用一下 == 为此目的。

        6
  •  18
  •   Dimitris Fasarakis Hilliard    8 年前

    我认为你的假设是正确的。实验用 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 被视为文字,上面的任何东西都被不同的对待!

        7
  •  12
  •   babbageclunk    16 年前

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

        8
  •  8
  •   Yann Vernier    9 年前

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

    PHP's === 另一方面,运算符被描述为检查相等性和类型: x == y and type(x) == type(y) 根据保罗·弗雷塔斯的评论。这对普通数字就足够了,但不同于 用于定义 __eq__ 以一种荒谬的方式:

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

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

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

    这将有另一个答案,因为我们必须将事物分类为数字或非数字,以选择是否与 == . CPython 定义 number protocol ,包括pynumber_检查,但不能从python本身进行访问。

    我们可以试着用 isinstance 对于我们所知道的所有数字类型,但这不可避免地是不完整的。“类型”模块包含字符串类型列表,但不包含数字类型。自Python2.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 . 检查对象标识,而不是值相等。PHP也有丰富的历史,其中 = = 显然表现为 仅在对象上 in PHP5, but not PHP4 . 这就是跨语言(包括一种语言的版本)移动所带来的日益增长的痛苦。

        9
  •  4
  •   sobolevn akiraak    9 年前

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

    >>> 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
  •  3
  •   user5319825    9 年前

    看一看 here

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

        11
  •  3
  •   abarnert    6 年前

    还有一个问题没有在任何现有答案中指出。python可以合并任意两个不可变的值,而预先创建的小int值并不是实现这一点的唯一方法。python实现永远不会 放心 这样做,但他们都做的不仅仅是小整数。


    首先,还有一些其他预先创建的值,例如空值 tuple , str bytes 和一些短字符串(在cpython 3.6中,它是256个单字符拉丁-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编译器将合并一些已知的不可变类型的常量值,如 int , 浮动 , STR , 字节 在同一编译单元中。对于一个模块来说,整个模块是一个编译单元,但是在交互解释器中,每个语句都是一个单独的编译单元。自从 c d 在单独的语句中定义,它们的值不会合并。自从 e f 在同一语句中定义,它们的值将被合并。


    你可以通过分解字节码看到发生了什么。尝试定义一个函数 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 和A 2 (1, 2) 共享相同的元组 但不完全相同, ((1, 2), (1, 2)) 具有两个不同的相等元组的元组。


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

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

    另一方面,它仅限于 STR 类型和字符串 internal storage kind "ascii compact", "compact", or "legacy ready" 而且在许多情况下,只有“ASCII压缩”会被Internet。


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

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

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

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