为什么下面的代码在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允许合并任何两个不可变的值,并且预先创建的小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()习惯用法)。

其他回答

这取决于你要看的是两个物体是否相等,还是同一个物体。

是检查它们是否是相同的对象,而不仅仅是相等。较小的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__属性来指定行为。

看看这个:

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

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

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

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

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

Is是恒等运算符(功能类似于id(a) == id(b));只是两个相等的数不一定是同一个物体。出于性能考虑,有些小整数会被记忆,所以它们往往是相同的(这可以做到,因为它们是不可变的)。

另一方面,PHP的===运算符被描述为检查相等性和type: x == y和type(x) == type(y),正如Paulo Freitas的注释所述。对于常见的数字,这就足够了,但与之不同的是,类以一种荒谬的方式定义__eq__:

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

PHP显然也允许“内置”类(我指的是在C级实现的,而不是在PHP中)。稍微不那么荒谬的用法可能是timer对象,它每次作为数字使用时都有不同的值。这就是为什么你想要模拟Visual Basic的Now而不是用time。time()来显示它是一个求值,我不知道。

Greg Hewgill (OP)做了一个澄清性的评论:“我的目标是比较对象的同一性,而不是价值相等。除了数字,我想把对象的同一性视为价值相等。”

这将有另一个答案,因为我们必须将事物分类为数字,以选择是否与==或is进行比较。CPython定义了数字协议,包括PyNumber_Check,但这不能从Python本身访问。

我们可以尝试对已知的所有数字类型使用isinstance,但这不可避免地是不完整的。types模块包含一个StringTypes列表,但没有NumberTypes。从Python 2.6开始,内置的数字类有一个基类numbers。数字,但它有同样的问题:

import numpy, numbers
assert not issubclass(numpy.int16,numbers.Number)
assert issubclass(int,numbers.Number)

顺便说一下,NumPy将生成低数字的单独实例。

其实我不知道这个问题的答案。我认为理论上可以使用ctypes来调用PyNumber_Check,但即使是这个函数也存在争议,而且它肯定是不可移植的。我们现在只需要对测试的内容不那么挑剔。

最后,这个问题源于Python最初没有一个类型树的谓词,如Scheme的数字?,或者Haskell的类型类Num. is检查对象的身份,而不是值是否相等。PHP也有丰富多彩的历史,在PHP5中===显然只对对象起作用,而PHP4中没有。这就是跨语言(包括一种语言的不同版本)的成长之痛。

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