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


当前回答

它也发生在字符串上:

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

这是意料之外的。

其他回答

它也发生在字符串上:

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

这是意料之外的。

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的主要用例——不要用它来测试整数、字符串、元组或其他类似的东西是否相等。

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,对象标识并不是特别有用。最好还是考虑一下平等。标识本质上是值对象的实现细节——因为它们是不可变的,所以对同一个对象的多次引用和对多个对象的多次引用之间没有有效的区别。