为什么下面的代码在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操作符可以区分两者。为什么会有漏洞的抽象?有什么更好的方法来比较两个任意物体来判断它们是否相同当我事先不知道它们是否是数字时?


当前回答

Python 3.8新增功能:Python行为的变化:

编译器现在在标识检查(is和 Is not)用于某些类型的字面量(例如字符串,int)。 在CPython中,这些通常可以意外地工作,但不能保证 该警告建议用户使用相等性测试(== 和!=)代替。

其他回答

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

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 not测试对象的同一性:x是y是真 当且仅当x和y是同一个对象。X不是y会得到 逆真值。

所以下面两个是等价的。

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

从文档中可以看到:

id 返回一个对象的“标识”。这是一个整数(或长 Integer),保证该对象的唯一性和常量 在它的生命周期内。两个生命周期不重叠的对象可能 具有相同的id()值。

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

那么用例是什么呢?PEP8描述:

与像None这样的单例对象的比较应该总是使用is或 不是,不是相等运算符。

这个问题

你问并陈述以下问题(带代码):

为什么下面的代码在Python中会出现意外的行为? >>> a = 256 >>> b = 256 >>> a是b #这是一个预期的结果

这不是一个预期的结果。为什么要这样做?它只意味着a和b引用的值为256的整数是同一个整数实例。整数在Python中是不可变的,因此它们不能改变。这应该不会对任何代码产生影响。这是不应该被期待的。它只是一个实现细节。

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

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

看起来我们现在在内存中有两个不同的整数实例,它们的值都是257。由于整数是不可变的,因此这会浪费内存。希望我们没有浪费太多时间。我们可能不是。但是这种行为并不能保证。

>>> 257等于257 True #但文字数字比较正确

好吧,这看起来像是你的特定Python实现试图变得聪明,除非必要,否则不会在内存中创建冗余值的整数。您似乎表明您正在使用Python的参考实现,即CPython。对CPython很好。

如果CPython能在全局范围内做到这一点,如果它能做到这一点(因为查找会有成本),那就更好了,也许另一个实现可以做到。

但至于对代码的影响,您不应该关心一个整数是否是一个整数的特定实例。您应该只关心该实例的值是什么,并且您可以使用普通的比较操作符,即==。

它做什么

是检查两个对象的id是否相同。在CPython中,id是内存中的位置,但它可以是其他实现中的其他唯一标识数字。用代码重申:

>>> a is b

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

为什么我们要用is呢?

This can be a very fast check relative to say, checking if two very long strings are equal in value. But since it applies to the uniqueness of the object, we thus have limited use-cases for it. In fact, we mostly want to use it to check for None, which is a singleton (a sole instance existing in one place in memory). We might create other singletons if there is potential to conflate them, which we might check with is, but these are relatively rare. Here's an example (will work in Python 2 and 3) e.g.

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被无参数调用,什么时候它被无参数调用。这些是is的主要用例——不要用它来测试整数、字符串、元组或其他类似的东西是否相等。

看看这个:

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

以下是我在“普通整数对象”的文档中找到的:

当前的实现为-5到256之间的所有整数保留了一个整数对象数组。当你在这个范围内创建一个int型时,你实际上只是得到了一个对现有对象的引用。

还有一个问题,在现有的任何答案中都没有指出。Python允许合并任何两个不可变的值,并且预先创建的小int值并不是发生这种情况的唯一方式。Python实现从来不保证这样做,但它们都不仅仅是对小int型进行这样做。


首先,还有一些其他预先创建的值,比如空元组、str和bytes,以及一些短字符串(在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提供预先创建的浮点值。那么,这里发生了什么?

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


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

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

另一方面,它仅限于str类型,以及内部存储类型为"ascii compact"、"compact"或"legacy ready"的字符串,并且在许多情况下只有"ascii compact"会被捕获。


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

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

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

或者,换句话说,唯一的用途是测试文档化的单例对象(如None)或只在代码中的一个地方创建的单例对象(如_sentinel = object()习惯用法)。

它也发生在字符串上:

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

这是意料之外的。