为什么以下在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在内部实现,使得"small"整数以不同于大整数的方式存储, is
运算符可以区分 . 为什么泄漏抽象?当我不知道它们是否是数字时,比较两个任意对象以查看它们是否相同的更好的方法是什么?
11 回答
你可以查一下在source file intobject.c中,Python缓存小整数以提高效率 . 每次创建对小整数的引用时,都指的是缓存的小整数,而不是新对象 . 257不是一个小整数,因此它被计算为一个不同的对象 .
为此目的最好使用
==
.总之 - 让我强调: Do not use is to compare integers.
这不是你应该有任何期望的行为 .
相反,使用
==
和!=
分别比较相等性和不等式 . 例如:解释
要了解这一点,您需要了解以下内容 .
首先,
is
做了什么?它是一个比较运算符 . 来自documentation:所以以下是等价的 .
来自documentation:
请注意,CPython中对象的id(Python的参考实现)是内存中的位置这一事实是一个实现细节 . Python的其他实现(例如Jython或IronPython)可以轻松地为
id
实现不同的实现 .那么
is
的用例是什么? PEP8 describes:问题
您询问并说明以下问题(带代码):
这不是预期的结果 . 为什么会这样?它只表示
a
和b
引用的256
值的整数是整数的相同实例 . 整数在Python中是不可变的,因此它们无法改变 . 这应该对任何代码都没有影响 . 不应该这样 . 它只是一个实现细节 .但也许我们应该感到高兴的是,每当我们声明一个值等于256时,内存中就没有新的单独实例 .
看起来我们现在有两个单独的整数实例,其内存值为
257
. 由于整数是不可变的,这会浪费内存 . 让's hope we'不要浪费太多 . 我们可能不是 . 但这种行为并不能保证 .好吧,这看起来像你的Python的特定实现是试图聪明,而不是在内存中创建冗余值的整数,除非它必须 . 您似乎表明您正在使用Python的引用实现,即CPython . 适合CPython .
如果CPython能够在全球范围内实现这一目标可能会更好,如果它可以做得那么便宜(因为在查找中会有成本),或许可能是另一种实现 .
但至于对代码的影响,您不应该关心整数是否是整数的特定实例 . 您应该只关心该实例的值是什么,并且您将使用正常的比较运算符,即
==
.是做什么的
is
检查两个对象的id
是否相同 . 在CPython中,id
是内存中的位置,但它可能是另一个实现中的其他唯一标识号 . 用代码重述这个:是相同的
为什么我们要使用呢?
相对于检查两个非常长的字符串是否相等,这可以是一个非常快速的检查 . 但由于它适用于对象的唯一性,因此我们对它的使用情况有限 . 事实上,我们主要想用它来检查
None
,它是一个单例(一个存在于内存中的唯一实例) . 我们可能会创建其他单身,如果有可能将它们混为一谈,我们可以查看is
,但这些是相对罕见的 . 这是一个例子(将在Python 2和3中使用),例如哪个印刷品:
所以我们看到,使用
is
和一个哨兵,我们能够区分何时bar
被调用而没有参数以及何时用None
调用 . 这些是is
的主要用例 - 不要用它来测试整数,字符串,元组或其他类似的东西的相等性 .看看这个:
编辑:这是我在Python 2文档中找到的,"Plain Integer Objects"(Python 3也是如此):
对于不可变值对象,如整数,字符串或日期时间,对象标识不是特别有用 . 考虑平等更好 . 身份本质上是值对象的实现细节 - 因为它们是不可变的,所以对同一个对象或多个对象进行多次引用之间没有任何有效的区别 .
看看here
我认为你的假设是正确的 . 试验
id
(对象的身份):似乎数字
<= 255
被视为文字,上面的任何内容都被区别对待!它也发生在字符串中:
现在一切都很好 .
这也是预期的 .
现在这出乎意料 .
is
是身份相等运算符(功能类似于id(a) == id(b)
);它's just that two equal numbers aren' t必然是同一个对象 . 出于性能原因,一些小整数碰巧是memoized所以它们往往是相同的(这可以做到,因为它们是不可变的) .另一方面,PHP's
===
运算符被描述为检查相等性和类型:x == y and type(x) == type(y)
,根据Paulo Freitas的评论 . 这对于常用数字就足够了,但对于以荒谬的方式定义__eq__
的类,is
不同:PHP显然允许"built-in"类使用相同的东西(我认为它意味着在C级实现,而不是在PHP中实现) . 稍微不那么荒谬的用法可能是一个计时器对象,每次它想要模拟Visual Basic的
Now
时都会有不同的值,而不是显示它是time.time()
的评估我不知道 .Greg Hewgill(OP)做了一个澄清评论“我的目标是比较对象身份,而不是 Value 的平等 . 除了数字,我想把对象身份看作是 Value 平等 . ”
这将有另一个答案,因为我们必须将事物分类为数字,以选择我们是否与
==
或is
进行比较 . CPython定义number protocol,包括PyNumber_Check,但这不能从Python本身访问 .我们可以尝试将
isinstance
与我们所知道的所有数字类型一起使用,但这不可避免地是不完整的 . types模块包含StringTypes列表但没有NumberTypes . 从Python 2.6开始,内置的数字类有一个基类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
. 跨越语言(包括版本的语言)越来越痛苦 .这取决于你是否想要看两件事是否相同,或者是同一个对象 .
is
检查它们是否是同一个对象,而不仅仅是相同的 . 小的int可能指向相同的内存位置以提高空间效率您应该使用
==
来比较任意对象的相等性 . 您可以使用__eq__
和__ne__
属性指定行为 .在任何现有的答案中都指出了's another issue that isn't . 允许Python合并任意两个不可变值,并且预先创建的小int值不是这种情况发生的唯一方法 . Python实现永远不能保证这样做,但它们都不仅仅是针对小的整数 .
首先,还有一些其他预先创建的值,例如空
tuple
,str
和bytes
,以及一些短字符串(在CPython 3.6中,它是256个单字符Latin-1字符串) . 对于例:但是,即使是非预先创建的值也可以是相同的 . 考虑这些例子:
这不仅限于
int
值:显然,CPython没有为
42.23e100
预先创建的float
值 . 那么,这里发生了什么?CPython编译器将在同一编译单元中合并一些已知不可变类型的常量值,如
int
,float
,str
,bytes
. 对于模块,整个模块是编译单元,但在交互式解释器中,每个语句都是一个单独的编译单元 . 由于c
和d
是在单独的语句中定义的,因此它们的值不会合并 . 由于e
和f
在同一语句中定义,因此它们的值将合并 .您可以通过反汇编字节码来查看正在发生的事情 . 尝试定义一个执行
e, f = 128, 128
然后在其上调用dis.dis
的函数,并且'll see that there'是一个常量值(128, 128)
您可能会注意到编译器已将
128
存储为常量,即使它是's not actually used by the bytecode, which gives you an idea of how little optimization CPython' s编译器也是如此 . 这意味着(非空)元组实际上不会最终合并:把它放在一个函数中,
dis
,看看co_consts
-there是1
和2
,两个(1, 2)
元组共享相同的1
和2
但是不相同,而((1, 2), (1, 2))
元组具有两个截然不同的元组 .CPython还有一个优化:string interning . 与编译器常量折叠不同,这不限于源代码文字:
另一方面,它仅限于
str
类型和internal storage kind "ascii compact", "compact", or "legacy ready"的字符串,并且在许多情况下只有"ascii compact"将被实现 .无论如何,对于什么值必须是,可能是或不可能是不同的规则,从实现到实现,以及相同实现的版本之间,甚至可能在同一实现的同一副本上运行相同代码之间 . .
为了它的乐趣,值得学习一个特定Python的规则 . 但是在代码中不值得依赖它们 . 唯一安全的规则是:
不要编写假设两个相等但单独创建的不可变值相同的代码 .
不要编写假设两个相等但单独创建的不可变值是不同的代码 .
或者,换句话说,只使用
is
来测试记录的单例(如None
)或仅在代码中的一个位置创建(如_sentinel = object()
成语) .我迟到了,你想要一些消息来源吗?*
关于CPython的好处是你实际上可以看到它的来源 . 我现在要使用
3.5
版本的链接;找到相应的2.x
是微不足道的 .在CPython中,处理创建新
int
对象的C-API
函数是PyLong_FromLong(long v) . 该功能的描述是:不知道你,但我看到了这一点,并想:让我们找到那个阵列!
如果您没有摆弄实现CPython的
C
代码,那么一切都非常有条理和可读 . 对于我们的情况,我们需要查看main source code directory tree的Objects/ subdirectory .PyLong_FromLong
处理long
对象,因此我们不应该很难推断出我们需要在longobject.c内窥视 . 在向内看之后,你可能会认为事情是混乱的;他们是,但不要害怕,我们正在寻找的功能在line 230等待我们检查出来 . 这是一个小功能,所以主体(不包括声明)很容易粘贴在这里:现在,我们没有
C
主码 - haxxorz,但我们也不傻,我们可以看到CHECK_SMALL_INT(ival);
诱惑地偷看我们;我们可以理解它与此有关 . Let's check it out:所以它是一个宏,如果值
ival
满足条件,则调用函数get_small_int
:那么
NSMALLNEGINTS
和NSMALLPOSINTS
是什么?如果你猜到了宏,你什么也得不到,因为那不是一个很难的问题.. Anyway, here they are:所以我们的条件是
if (-5 <= ival && ival < 257)
来电get_small_int
.没有其他地方可以继续我们的旅程,看看get_small_int in all its glory(好吧,我们'll just look at it'的身体,因为这是有趣的事情):
好的,声明
PyObject
,断言先前的条件成立并执行赋值:small_ints
看起来很像我们一直在寻找的那个阵列..而且,它是! We could've just read the damn documentation and we would've know all along!:所以,是的,这是我们的家伙 . 如果要在
[NSMALLNEGINTS, NSMALLPOSINTS)
范围内创建新的int
,则只需返回对已预先分配的现有对象的引用 .由于参考指的是同一个对象,直接发出
id()
或用is
检查身份将返回完全相同的东西 .但是,什么时候分配?
During initialization in _PyLong_Init Python很乐意进入for循环这样做:
我希望我的解释现在能够清楚地表达你的意思 .
但是,257是257?这是怎么回事?
这实际上更容易解释,and I have attempted to do so already;这是因为Python将执行这个交互式语句:
作为单个块 . 在对此声明进行编译时,CPython将看到您有两个匹配的文字,并将使用相同的
PyLongObject
表示257
. 如果您自己编译并检查其内容,您可以看到这个:当CPython进行操作时;它现在只是加载完全相同的对象:
所以
is
将返回True
.