【问题标题】:"is" operator behaves unexpectedly with integers"is" 运算符对整数的行为异常
【发布时间】:2010-09-23 07:23:24
【问题描述】:

为什么在 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 运算符可以区分。为什么有泄漏的抽象?当我事先不知道它们是否是数字时,比较两个任意对象以查看它们是否相同,有什么更好的方法?

【问题讨论】:

  • 看看here > 当前实现为所有 > -5 到 256 之间的整数保留一个整数对象数组,当您在该范围内创建一个 int 时,您 > 实际上只是取回一个引用到现有对象。
  • 这是一个 CPython 特定的实现细节和未定义的行为,谨慎使用
  • 这能回答你的问题吗? Is there a difference between "==" and "is"?

标签: python int operators identity python-internals


【解决方案1】:

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

【讨论】:

  • 这是因为表示从 -5 到 +256 的值的对象是在启动时创建的 - 因此所有这些值的使用都用于预构建的对象。几乎所有对该范围之外的整数的引用都会在每次引用它们时创建一个新的内部对象。我认为字面量这个词的使用令人困惑——字面量通常是指在一段代码中输入的任何值——所以源代码中的所有数字都是字面量。
【解决方案2】:

看看这个:

>>> 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 在 这种情况是未定义的。 :-)

【讨论】:

  • 有谁知道这个范围 (-5, 256) 是如何选择的?如果它是 (0, 255) 甚至 (-255, 255),我不会太惊讶,但是从 -5 开始的 262 个数字的范围似乎令人惊讶地随意。
  • @WoodrowBarlow:我认为,-5 只是捕捉常见负占位符的启发式方法。 0..255 涵盖单字节值的数组。神秘的是 256,但我猜它是为了(反)将整数组装成/从字节中。
  • 据我了解,该范围是通过查看多个项目(和多种语言)的常用值来选择的。
  • 根据reddit.com/r/Python/comments/18leav/…,范围以前是[-5,100]。它被扩展为包括所有字节值 - 加上 256,因为这可能是一个常见的数字。
  • 关于更改1 值的注释显然是个笑话,因为Python 中的整数是不可变的。该短语不在最新文档中。
【解决方案3】:

这取决于您是要查看 2 个事物是否相等,还是相同的对象。

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

    您可以在source file intobject.c 中查看,Python 缓存小整数以提高效率。每次创建对小整数的引用时,您指的是缓存的小整数,而不是新对象。 257不是小整数,所以算作不同的对象。

    最好使用== 来达到这个目的。

    【讨论】:

      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

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

        另一方面,PHP's === 运算符被描述为检查相等性和类型:x == y and type(x) == type(y) 根据 Paulo Freitas 的评论。这对于普通数字就足够了,但对于以荒谬的方式定义 __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 开始,内置的 number 类有一个基类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。这就是跨语言(包括一种语言的版本)迁移的痛苦。

        【讨论】:

          【解决方案7】:

          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 返回对象的“身份”。这是一个整数(或长 integer) 保证对于这个对象是唯一且恒定的 在其生命周期内。两个生命周期不重叠的对象可能 具有相同的id() 值。

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

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

          None 等单例的比较应始终使用isis not,绝不是相等运算符。

          问题

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

          为什么在 Python 中会出现以下异常行为?

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

          不是预期的结果。为什么是预期的?这仅意味着ab 引用的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 和一个哨兵,我们能够区分何时调用不带参数的bar 和何时调用None。这些是is 的主要用例 - 使用它来测试整数、字符串、元组或其他类似事物的相等性。

          【讨论】:

          • “这些是 is 的主要用例 - 不要用它来测试整数、字符串、元组或其他类似事物的相等性。”但是,我正在尝试将一个简单的状态机集成到我的类中,并且由于状态是不透明的值,其唯一可观察的属性是相同或不同的属性,因此它们看起来很自然地与is 相提并论。我计划使用实习字符串作为状态。我会更喜欢纯整数,但不幸的是 Python 不能实习整数(0 is 0 是一个实现细节)。
          • @Alexey 听起来你需要枚举? stackoverflow.com/questions/37601644/…
          • 也许,谢谢,不知道他们。这可能是您回答 IMO 的适当补充。
          • 也许在你的答案中使用一些像哨兵这样的愚蠢对象会是一个更轻量级的解决方案......
          • @Alexey 枚举在 Python 3 标准库中,这可能会鼓励您的代码比单纯的哨兵更有意义。
          【解决方案8】:

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

          >>> 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' 并非如此。
          • 那是因为如果没有空格,它看起来就像一个符号。名称会自动被实习,因此如果在 Python 会话中的任何位置有任何名为 xx 的内容,则该字符串已被实习;如果它只是类似于一个名字,可能会有一个启发式方法来做到这一点。与数字一样,这是可以做到的,因为它们是不可变的。 docs.python.org/2/library/functions.html#internguilload.com/python-string-interning
          【解决方案9】:

          我迟到了,但是你想知道答案的来源吗?我会尝试以介绍性的方式说出这个,以便更多人可以跟进。


          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 内部。往里看后,你可能会认为事情很混乱;他们是,但不要害怕,我们正在寻找的功能在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)
          

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

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

          那么NSMALLNEGINTSNSMALLPOSINTS 是什么?宏! Here they are

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

          所以我们的条件是if (-5 &lt;= ival &amp;&amp; ival &lt; 257) call 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];
          

          是的,这是我们的人。当您想在[NSMALLNEGINTS, NSMALLPOSINTS) 范围内创建一个新的int 时,您只需取回对已预先分配的现有对象的引用。

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

          但是,它们是什么时候分配的??

          During initialization in _PyLong_InitPython 很乐意进入 for 循环为您执行此操作:

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

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

          我希望我的解释现在已经让你 C 清楚了事情(双关语显然是有意的)。


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

          【讨论】:

            【解决方案10】:

            任何现有答案中都没有指出另一个问题。 Python 允许合并任何两个不可变值,并且预先创建的小 int 值并不是发生这种情况的唯一方法。 Python 实现永远不会保证做到这一点,但它们都不仅仅是为了小整数。


            一方面,还有一些其他预先创建的值,例如空的tuplestrbytes,以及一些短字符串(在 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 没有为42.23e100 预先创建float 值。那么,这里发生了什么?

            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 类型和internal storage kind "ascii compact", "compact", or "legacy ready" 的字符串,并且在许多情况下只有“ascii compact”会被实习。


            无论如何,关于什么值必须是、可能是或不能是不同的规则因实现而异,在同一实现的不同版本之间,甚至可能在同一代码副本上运行相同代码之间有所不同。相同的实现。

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

            • 不要编写假定两个相等但单独创建的不可变值相同的代码(不要使用x is y,使用x == y
            • 不要编写假定两个相等但单独创建的不可变值是不同的代码(不要使用x is not y,使用x != y

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

            【讨论】:

            • 不那么神秘的建议很简单:不要使用x is y进行比较,使用x == y。同样不要使用x is not y,使用x != y
            • 那么看this question,为什么a=257; b=257在一行a is b是真的
            【解决方案11】:

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

            编译器现在在身份检查时生成SyntaxWarningisis not) 与某些类型的文字(例如字符串、整数)一起使用。 这些通常可以在 CPython 中意外工作,但不能保证 语言规范。警告建议用户使用相等测试 (==!=) 代替。

            【讨论】:

              猜你喜欢
              • 2022-10-07
              • 2016-03-12
              • 2016-12-14
              相关资源
              最近更新 更多