问题 在python中IS运算符的罕见行为


从Stackoverflow上的一些答案中,我开始知道从-5到256相同的内存位置被引用,因此我们得到了:

>>> a = 256
>>> a is 256
True

现在来了扭曲(在标记重复之前参见此行):

>>> a = 257
>>> a is 257 
False 

这是完全理解的,但现在如果我这样做:

>>> a = 257; a is 257
True
>>> a = 12345; a is 12345
True

为什么?


1357
2018-06-15 19:33


起源

我敢打赌,因为翻译可以“看到”并分析 都 同时,Python可以做一些优化来完全消除变量并做类似的事情 12345 is 12345。 - ForceBru
...或不: import dis; dis.disco(compile('a=P;a is P', '', 'exec')) 为256和12345返回类似的程序集... - ForceBru
不是“解释器”,而是“编译器”。 - Ignacio Vazquez-Abrams
这个问题很史诗。我们得出了非常好的结论。谢谢你的询问! - Attersson
我是Stackoverflow的新手,我非常感谢这个社区。 - RICKY


答案:


你所看到的是CPython中编译器的优化(它将你的源代码编译成解释器运行的字节码)。每当在一个步骤中编译的一块代码中的几个不同位置使用相同的不可变常量值时,编译器将尝试对每个位置使用对同一对象的引用。

因此,如果您在交互式会话中在同一行上执行多个赋值,您将获得对同一对象的两个引用,但如果您使用两个单独的行,则不会:

>>> x = 257; y = 257  # multiple statements on the same line are compiled in one step
>>> print(x is y)     # prints True
>>> x = 257
>>> y = 257
>>> print(x is y)     # prints False this time, since the assignments were compiled separately

这种优化出现的另一个地方是函数体。整个函数体将被编译在一起,因此函数中任何地方使用的任何常量都可以组合,即使它们位于不同的行上:

def foo():
    x = 257
    y = 257
    return x is y  # this will always return True

虽然调查像这样的优化是有趣的,但你不应该在普通代码中依赖这种行为。不同的Python解释器,甚至不同版本的CPython可能会以不同方式进行这些优化,或者根本不进行。如果您的代码依赖于特定的优化,那么对于试图在自己的系统上运行它的其他人来说,它可能会完全被破坏。

作为一个例子,我在上面的第一个代码块中显示的同一行上的两个赋值当我在Spyder(我首选的IDE)中的交互式shell中执行时,不会导致对同一对象的两个引用。我不知道为什么这种特定情况与传统交互式shell中的情况不同,但不同的行为是我的错,因为我的代码依赖于特定于实现的行为。


7
2018-06-15 20:54





来自python2文档:

运算符是和不测试对象标识:x是y是真的   当且仅当x和y是同一个对象时。 x不是y得到的   反向真值。 [6]

来自python3文档:

运算符是和不测试对象标识:x是y是真的   当且仅当x和y是同一个对象时。对象标识是   使用id()函数确定。 x不是y得到逆   真相价值。 [4]

所以基本上理解你在repl控制台上运行的测试的关键是使用 相应的 id() 功能,这里有一个例子,它将告诉你窗帘背后发生了什么:

>>> a=256
>>> id(a);id(256);a is 256
2012996640
2012996640
True
>>> a=257
>>> id(a);id(257);a is 257
36163472
36162032
False
>>> a=257;id(a);id(257);a is 257
36162496
36162496
True
>>> a=12345;id(a);id(12345);a is 12345
36162240
36162240
True

也就是说,通常使用这些类型的片段了解窗帘背后发生的事情的一个好方法是使用 dis.dis 要么 dis.disco,让我们来看看这个代码片段的样子:

import dis
import textwrap

dis.disco(compile(textwrap.dedent("""\
    a=256
    a is 256
    a=257
    a is 257
    a=257;a is 257
    a=12345;a is 12345\
"""), '', 'exec'))

输出将是:

  1           0 LOAD_CONST               0 (256)
              2 STORE_NAME               0 (a)

  2           4 LOAD_NAME                0 (a)
              6 LOAD_CONST               0 (256)
              8 COMPARE_OP               8 (is)
             10 POP_TOP

  3          12 LOAD_CONST               1 (257)
             14 STORE_NAME               0 (a)

  4          16 LOAD_NAME                0 (a)
             18 LOAD_CONST               1 (257)
             20 COMPARE_OP               8 (is)
             22 POP_TOP

  5          24 LOAD_CONST               1 (257)
             26 STORE_NAME               0 (a)
             28 LOAD_NAME                0 (a)
             30 LOAD_CONST               1 (257)
             32 COMPARE_OP               8 (is)
             34 POP_TOP

  6          36 LOAD_CONST               2 (12345)
             38 STORE_NAME               0 (a)
             40 LOAD_NAME                0 (a)
             42 LOAD_CONST               2 (12345)
             44 COMPARE_OP               8 (is)
             46 POP_TOP
             48 LOAD_CONST               3 (None)
             50 RETURN_VALUE

正如我们在这种情况下可以看到的那样,asm输出并没有告诉我们,我们可以看到,第3-4行基本上是与第5行相同的“相同”指令。所以我的建议将再次使用 id()聪明地让你知道是什么 is 会比较。如果你想知道cpython正在做的优化类型,我担心你需要挖掘它的 源代码


1
2018-06-15 19:55



问题是, 为什么 ID的行为是这样的吗?为什么分别执行两个语句会给出不同的ID,但是将它们作为同一行执行会产生相同的ID? - ForceBru
你可能想要研究的东西: a = 257; a is 257 在3.6.4中,当我在Windows上运行时返回True,但在Jupyter笔记本中运行时返回False - bphi
@bphi这很有趣,我想在Jupyter笔记本中生成的asm与我的回答中发布的相同,对吧? - BPL
@bphi是的确切...... - Attersson
@BPL dis.disco提供相同的输出,但第3和第4个示例返回False而不是True - bphi


经过各种版本的讨论和测试,可以得出最终结论。

Python将以块的形式解释和编译指令。根据所使用的语法,Python版本,操作系统,分发,可以实现不同的结果,具体取决于Python在一个块中采用的指令。

一般规则是:

(从 官方文件

当前实现为所有实体保留了一个整数对象数组   -5到256之间的整数

因此:

a = 256
id(a)
Out[2]: 1997190544
id(256)
Out[3]: 1997190544 # int actually stored once within Python

a = 257
id(a)
Out[5]: 2365489141456
id(257)
Out[6]: 2365489140880 #literal, temporary. as you see the ids differ
id(257)
Out[7]: 2365489142192 # literal, temporary. as you see it gets a new id everytime
                      # since it is not pre-stored

下面的部分返回False Python 3.6.3 | Anaconda自定义(64位)| (默认,2017年10月17日,23:26:12)[MSC v.1900 64 bit(AMD64)]

a = 257; a is 257
Out[8]: False

a=257; print(a is 257) ; a=258; print(a is 257)
>>>True
>>>False

很明显,无论Python在“一个块”中采用什么都是非确定性的,并且可以根据它的编写方式,单线或不一样,以及所使用的版本,操作系统和分布而摇摆不定。


1
2018-06-15 19:44



我得到了真的 a = x; a is x 对于3.6上的任何x,任何想法之间有什么区别,并将它分成两行? - bphi
无法使用Python 3.6.1确认您的上一个结果 - ForceBru
我正在使用Python 3.6.3并且认真,你能安装最新版本并验证吗?我们可能会遇到实施方面的差异 - Attersson
谢谢。这意味着在最新版本中,Python采用了一种在线缓冲,以避免重新声明相同的符号并在内存中复制 - Attersson
它实际上不是一行一行的。它是一次性编译的。例如,函数的主体通常只需一步编译即可 def foo():x=1000;y=1000;return x is y (使用换行符和适当的缩进)将为两者使用相同的常量整数对象 x 和 y等等回归 True。 - Blckknght


一般而言,-5至256范围之外的数字不一定具有应用于该范围内的数字的优化。但是,Python可以根据需要自由应用其他优化。在你的事业中,你会发现在一行中多次使用的相同文字值存储在一个内存位置,无论它在该行上使用了多少次。以下是此行为的其他一些示例:

>>> s = 'a'; s is 'a'
True
>>> s = 'asdfghjklzxcvbnmsdhasjkdhskdja'; s is 'asdfghjklzxcvbnmsdhasjkdhskdja'
True
>>> x = 3.14159; x is 3.14159
True
>>> t = 'a' + 'b'; t is 'a' + 'b'
True
>>> 

0
2018-06-15 19:52