591

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

4

11 回答 11

432

看看这个:

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

于 2008-11-20T18:30:20.017 回答
139

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做什么?它是一个比较运算符。从文档中:

对象身份的运算符is和测试:当且仅当 x 和 y 是同一个对象时为真。产生逆真值。is notx is yx is not y

所以以下是等价的。

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

文档中:

id 返回对象的“身份”。这是一个整数(或长整数),保证该对象在其生命周期内是唯一且恒定的。具有不重叠生命周期的两个对象可能具有相同的id()值。

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

那么用例是is什么? PEP8 描述

与单例的比较None应该总是使用isor is not,而不是相等运算符。

问题

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

为什么以下在 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 可以在全球范围内执行此操作可能会更好,如果它可以如此便宜地执行(因为查找会产生成本),也许另一种实现可能会更好。

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

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- 不要它来测试整数、字符串、元组或其他类似事物的相等性。

于 2015-03-04T20:20:12.797 回答
69

我迟到了,但是,你想要一些你的答案的来源吗?我会尝试以介绍性的方式表达这一点,以便更多人可以跟进。


CPython 的一个好处是您实际上可以看到它的源代码。我将使用3.5版本的链接,但找到相应的2.x版本是微不足道的。

在 CPython 中,处理创建新对象的C-API函数是. 这个函数的描述是:intPyLong_FromLong(long v)

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

(我的斜体)

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

如果您还没有摆弄实现 CPython 的 C 代码,那么您应该;一切都井井有条,可读性强。对于我们的例子,我们需要查看主源代码目录树Objects子目录

PyLong_FromLong处理long对象,因此不难推断我们需要窥视内部longobject.c。往里看后,你可能会认为事情很混乱;他们是,但不要害怕,我们正在寻找的功能正在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);诱人地偷看我们;我们可以理解这与此有关。让我们来看看:

#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?宏!他们在这里

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

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

接下来让我们看看get_small_int它的所有荣耀(好吧,我们只看它的身体,因为那是有趣的地方):

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看起来很像我们一直在寻找的那个数组,它就是!我们本可以只阅读该死的文档,我们就会一直知道!

/* 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将返回完全相同的东西。

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

_PyLong_InitPython 的初始化过程中,我们很乐意输入一个 for 循环来为您执行此操作:

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

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

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


但是,257 is 257? 这是怎么回事?

这实际上更容易解释,我已经尝试过这样做;这是因为 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

于 2016-01-23T13:26:00.273 回答
61

这取决于您是否要查看 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__属性指定行为。

于 2008-11-20T18:36:06.977 回答
38

正如您可以检查源文件intobject.c 一样,Python 缓存小整数以提高效率。每次创建对小整数的引用时,您指的是缓存的小整数,而不是新对象。257不是一个小整数,所以计算为不同的对象。

最好==用于此目的。

于 2008-11-20T19:50:11.337 回答
21

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

于 2008-11-20T18:29:48.583 回答
14

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

于 2008-11-21T01:58:53.853 回答
14

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


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

CPython 编译器将在同一个编译单元中合并一些已知不可变类型的常量值,例如int, float, str, 。bytes对于一个模块,整个模块是一个编译单元,但在交互式解释器中,每条语句都是一个单独的编译单元。由于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)元组共享相同但不相同,一个1元组有两个不同的相等元组。2((1, 2), (1, 2))


CPython 还做了一项优化:字符串实习。与编译器常量折叠不同,这不仅限于源代码文字:

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

另一方面,它受限于类型和内部存储类型“ascii compact”、“compact”或“legacy ready”str的字符串,在许多情况下,只有“ascii compact”会被实习。


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

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

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

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

于 2018-03-25T03:48:45.813 回答
8

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

===另一方面,PHP 的x == y and type(x) == type(y)运算符被描述为检查相等性和类型:根据 Paulo Freitas 的评论。这对于常见的数字就足够了,但与以荒谬方式is定义的类不同:__eq__

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

PHP 显然允许对“内置”类进行同样的操作(我认为这是在 C 级别实现的,而不是在 PHP 中实现的)。一个稍微不那么荒谬的使用可能是一个计时器对象,它每次用作数字时都有不同的值。这就是为什么您要模拟 Visual BasicNow而不是显示它是time.time()我不知道的评估的原因。

Greg Hewgill (OP) 发表了一条澄清评论:“我的目标是比较对象身份,而不是价值平等。除了数字,我想将对象身份视为价值平等。”

这将有另一个答案,因为我们必须将事物归类为数字,以选择我们是否与==或进行比较isCPython定义了数字协议,包括 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,但即使是那个函数也有争议,而且它肯定不是可移植的。我们只需要对我们现在测试的内容不那么挑剔。

最后,这个问题源于 Python 最初没有具有像Scheme number?Haskell 的 类型类 Num这样的谓词的类型树。is检查对象身份,而不是值相等。PHP 也有一段丰富多彩的历史,===显然只表现在 PHP5is中的对象上,而不是 PHP4上。这就是跨语言(包括一种语言的版本)移动的日益增长的痛苦。

于 2013-03-20T11:20:46.180 回答
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)

现在这是出乎意料的。

于 2015-10-14T15:53:05.600 回答
4

Python 3.8 的新特性:Python 行为的变化

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

于 2019-08-24T20:34:16.037 回答