为什么以下在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
运算符可以区分。为什么泄漏抽象?当我不知道它们是否是数字时,有什么比较比较两个任意对象以查看它们是否相同的更好方法?
答案 0 :(得分:345)
看看这个:
>>> 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的行为 这种情况是未定义的。 : - )
答案 1 :(得分:91)
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
做了什么?它是一个比较运算符。来自documentation:
运算符
is
和is not
测试对象标识:x is y
为真 当且仅当x和y是同一个对象时。x is not y
产生了 反向真值。
以下是相同的。
>>> a is b
>>> id(a) == id(b)
<强>
id
强> 返回对象的“标识”。这是一个整数(或长整数) 整数)保证对于该对象是唯一的和常量的 在其一生中。两个具有非重叠寿命的对象可以 具有相同的id()
值。
请注意,CPython中对象的id(Python的参考实现)是内存中的位置这一事实是一个实现细节。 Python的其他实现(例如Jython或IronPython)可以轻松地为id
实现不同的实现。
那么is
的用例是什么? PEP8 describes:
与
None
等单身人士的比较应始终使用is
或。{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
的主要用例 - 执行不使用它来测试整数,字符串,元组或其他类似内容的相等性。
答案 2 :(得分:56)
这取决于您是否想要查看两件事情是否相同,或者是同一个对象。
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__
属性指定行为。
答案 3 :(得分:40)
我迟到了,你想要一些消息来源吗? *
关于CPython的好处是你实际上可以看到它的来源。我现在要使用3.5
版本的链接;找到相应的2.x
是微不足道的。
在CPython中,处理创建新C-API
对象的int
函数是PyLong_FromLong(long v)
。该功能的描述是:
当前实现为-5到256之间的所有整数保留一个整数对象数组,当您在该范围内创建一个int时,实际上只返回对现有对象的引用。因此应该可以更改值1.我怀疑在这种情况下Python的行为是未定义的。 : - )
不知道你,但我看到了这一点,并想:让我们找到那个数组!
如果你没有摆弄实现CPython 的C
代码,你应该,一切都非常有条理和可读。对于我们的情况,我们需要查看Objects/
subdirectory的main source code directory tree。
PyLong_FromLong
处理long
个对象,因此不难推断我们需要查看longobject.c
内部。看完后你可能会认为事情很混乱;他们是,但不要担心,我们正在寻找的功能在line 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);
诱惑地偷看我们;我们可以理解它与此有关。 Let's check it out:
#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
是什么?如果您猜到了宏,那么您什么也得不到,因为这不是一个很难的问题。 Anyway, here they are :
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
所以我们的条件是if (-5 <= ival && ival < 257)
来电get_small_int
。
没有其他地方可去但是继续我们的旅程看get_small_int
in all its glory(好吧,我们只看它的身体,因为这是有趣的事情):
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
看起来很像我们一直在搜索的那个数组......而且,它是! We could've just read the damn documentation and we would've know all along! :
/* 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
的身份将返回完全相同的内容。
During initialization in _PyLong_Init
Python很乐意进入for循环,为你做这个:
for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++, v++) {
// Look me up!
}
我希望我的解释现在能清楚地表明你C
(双关语显然是有意的)。
这实际上更容易解释,and I have attempted to do so already;这是因为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
。
* - 我会尝试以更具介意性的方式说出这一点,以便大多数人能够跟进。
答案 4 :(得分:36)
正如您可以检查source file intobject.c一样,Python会缓存小整数以提高效率。每次创建对小整数的引用时,都指的是缓存的小整数,而不是新对象。 257不是一个小整数,因此它被计算为一个不同的对象。
最好为此目的使用==
。
答案 5 :(得分:18)
我认为你的假设是正确的。试验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
被视为文字,上面的任何内容都被区别对待!
答案 6 :(得分:12)
对于不可变值对象,如整数,字符串或日期时间,对象标识不是特别有用。考虑平等更好。身份本质上是值对象的实现细节 - 因为它们是不可变的,所以对同一个对象或多个对象有多个引用之间没有任何有效的区别。
答案 7 :(得分:8)
is
是身份相等运算符(功能类似于id(a) == id(b)
);只是两个相等的数字不一定是同一个对象。出于性能原因,一些小整数碰巧是memoized所以它们往往是相同的(这可以做到,因为它们是不可变的)。
PHP's ===
运算符被描述为检查相等性并按照Paulo Freitas的评论键入x == y and type(x) == type(y)
。这对于常见数字就足够了,但对于以荒谬方式定义is
的类,__eq__
不同:
class Unequal:
def __eq__(self, other):
return False
PHP显然允许“内置”类使用相同的东西(我认为它意味着在C级实现,而不是在PHP中实现)。稍微不那么荒谬的用法可能是一个计时器对象,每次用作数字时它都有不同的值。你为什么要模仿Visual Basic的Now
,而不是表明它是time.time()
我不知道的评价。
Greg Hewgill(OP)做了一个澄清评论“我的目标是比较对象身份,而不是价值的平等。除了数字,我想把对象身份看作是价值平等。”
这还有另一个答案,因为我们必须将事物分类为数字,以选择是否与==
或is
进行比较。 CPython定义了number protocol,包括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
,但即使是那个函数has been debated,它肯定不是可移植的。我们必须对我们现在测试的内容不那么特别。
最后,这个问题源于Python最初没有包含Scheme's number?
或Haskell's type class Num等谓词的类型树。 is
检查对象标识,而不是值相等。 PHP也有丰富多彩的历史记录,其中===
仅在对象in PHP5, but not PHP4上表现为is
。跨越语言(包括版本的语言)越来越痛苦。
答案 8 :(得分:5)
在现有的任何答案中都没有指出另一个问题。允许Python合并任意两个不可变值,并且预先创建的小int值不是这种情况发生的唯一方式。 Python实现永远不会保证来执行此操作,但它们都不仅仅是为了实现这一点。
首先,还有一些其他预先创建的值,例如空tuple
,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没有为float
预先创建的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还有一个优化:string interning。与编译器常量折叠不同,这不仅限于源代码文字:
>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True
另一方面,它仅限于str
类型和internal storage kind "ascii compact", "compact", or "legacy ready"的字符串,并且在很多情况下只有“ascii compact”会被实习。
无论如何,对于什么值必须是,可能是或不可能是不同的规则,从实现到实现,以及相同实现的版本之间,甚至可能在相同副本的相同代码之间运行。同样的实施。
为了它的乐趣,值得学习一个特定Python的规则。但是在代码中不值得依赖它们。唯一安全的规则是:
或者换句话说,只使用is
来测试记录的单例(例如None
)或仅在代码中的一个位置创建(例如_sentinel = object()
成语)。
答案 9 :(得分: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)
现在出乎意料。
答案 10 :(得分:2)
What’s New In Python 3.8: Changes in Python behavior:
现在,在进行身份检查(
is
和is not
)用于某些类型的文字(例如字符串,整数)。 这些通常在CPython中偶然地起作用,但不能保证 语言规范。该警告建议用户使用相等性测试(==
和!=
)。