“女儿家;运算符对整数的行为异常

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

99968 次浏览

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

看看这个:

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

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

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

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

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

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

为此,最好使用==

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

is 单位相等操作符(功能类似id(a) == id(b));只是两个相等的数不一定是同一个物体。出于性能原因,一些小整数恰好是memoize的,因此它们往往是相同的(这可以做到,因为它们是不可变的)。

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

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.Number,但它有同样的问题:

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

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

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

最后,这个问题源于Python最初没有一个带有计划的 number?Haskell的 类型的类 全国矿工工会等谓词的类型树。is检查对象标识,而不是值是否相等。PHP也有丰富多彩的历史,===显然只在对象PHP5,而PHP4上表现为is。这就是跨语言(包括一种语言的不同版本)的成长之痛。

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

运算符isis not测试对象身份:x is y为真 当且仅当x和y是同一个对象。x is not y将产生

所以下面两个是等价的。

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

文档:

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

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

那么,is的用例是什么?# EYZ1:

None这样的单例对象的比较应该始终使用is

. is 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调用bar。这些是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)

这是意料之外的。

我迟到了,但你想要一些答案来源吗?我会试着用一种介绍性的方式来表达,这样更多的人可以跟上。


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

在CPython中,处理创建一个新的int对象的c api函数是PyLong_FromLong(long v)。该函数的描述如下:

# EYZ0。所以应该可以改变1的值。我怀疑Python在这种情况下的行为是未定义的。: -)

(斜体)

不知道你怎么想,但我看到这个就想:让我们找到这个数组!

如果你还没有修改过实现CPython 你应该的C代码;一切都很有条理,可读。对于我们的例子,我们需要在主要源代码目录树# EYZ0子目录中查找。

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# EYZ1,但我们也不傻,我们可以看到CHECK_SMALL_INT(ival);在诱人地窥视我们;我们可以理解它与此有关。# EYZ2

#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是什么?宏!# EYZ2:

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

所以我们的条件是if (-5 <= ival && ival < 257)调用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看起来很像我们一直在搜索的数组,它确实是!# EYZ1:

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

但是,什么时候分配呢?

_PyLong_Init初始化时 Python会很高兴地进入for循环为你做这件事:

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

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

我希望我的解释已经让你清楚C的事情(双关语明显的意图)。


但是,# EYZ0吗?有什么事吗?

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

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


首先,还有一些其他预先创建的值,比如空的tuplestrbytes,以及一些短字符串(在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提供预先创建的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它,然后看# eyz1 -有一个12,两个(1, 2)元组共享相同的12,但不相同,还有一个((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 is not y,使用x != y)

或者,换句话说,只使用is来测试记录的单例(如None)或只在代码中的一个地方创建的单例(如_sentinel = object()习惯用法)。

# EYZ0:

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

. !=)