为什么“is”运算符在脚本中的行为与 REPL 不同?

Why does the `is` operator behave differently in a script vs the REPL?

提问人:Bokyun Na 提问时间:3/26/2019 最后编辑:wimBokyun Na 更新时间:7/9/2022 访问量:897

问:

在 python 中,两个代码有不同的结果:

a = 300
b = 300
print (a==b)
print (a is b)      ## print True
print ("id(a) = %d, id(b) = %d"%(id(a), id(b))) ## They have same address

但是在shell模式(交互模式)下:

>>> a = 300
>>> b = 300
>>> a is b
False
>>> id(a)
4501364368
>>> id(b)
4501362224

“is”运算符具有不同的结果。

蟒蛇 cpython

评论

1赞 torek 3/26/2019
@Aran-Fey:以这种方式比较什么类型的对象并不那么重要,但在这种特殊情况下,这是因为编译源文件的实体注意到 300 == 300 因此只创建了一个 300 实例,而读取这些行的解释器没有注意到 300 == 300 因此创建了两个单独的 300 实例。.py>>>
3赞 Chris 3/26/2019
尽管如此,还是用于对象标识。只在你想要的时候使用它。is
3赞 Aran-Fey 3/26/2019
@Chris 我认为这也没有真正回答这个问题。就像 torek 说的,这个问题是关于在 REPL 中运行相同的代码与作为脚本运行相同的代码之间的区别。
5赞 Ondrej K. 3/26/2019
正如@Chris所说。换句话说,两个具有相同值的不可变对象的标识是偶然的,不能保证/必需。因此,为什么它一次匹配而一次不匹配是无关紧要的。
1赞 John Szakmeister 3/26/2019
伙计们,这些答案都没有真正回答这个问题。:-(

答:

16赞 wim 3/26/2019 #1

在脚本中运行代码时,整个文件在执行之前会编译为代码对象。在这种情况下,CPython 能够进行某些优化 - 例如对整数 300 重用相同的实例。.py

您还可以在 REPL 中重现它,方法是在更接近脚本执行的上下文中执行代码:

>>> source = """\ 
... a = 300 
... b = 300 
... print (a==b) 
... print (a is b)## print True 
... print ("id(a) = %d, id(b) = %d"%(id(a), id(b))) ## They have same address 
... """
>>> code_obj = compile(source, filename="myscript.py", mode="exec")
>>> exec(code_obj) 
True
True
id(a) = 140736953597776, id(b) = 140736953597776

其中一些优化非常激进。您可以修改脚本行,将其更改为 ,CPython 仍将“折叠”到相同的常量中。如果您对此类实现细节感兴趣,请在 peephole.c 和 Ctrl+F 中查找有关“consts 表”的任何信息。b = 300b = 150 + 150bPyCode_Optimize

相反,当您直接在 REPL 中逐行运行代码时,它会在不同的上下文中执行。每行都以“单”模式编译,此优化不可用。

>>> scope = {} 
>>> lines = source.splitlines()
>>> for line in lines: 
...     code_obj = compile(line, filename="<I'm in the REPL>", mode="single")
...     exec(code_obj, scope) 
...
True
False
id(a) = 140737087176016, id(b) = 140737087176080
>>> scope['a'], scope['b']
(300, 300)
>>> id(scope['a']), id(scope['b'])
(140737087176016, 140737087176080)

评论

0赞 ead 3/27/2019
在 Python3.6 中没有优化,但在更高版本中。b=150+150
6赞 John Szakmeister 3/26/2019 #2

关于 CPython 及其行为,实际上有两件事需要了解。 首先,[-5, 256] 范围内的小整数在内部被隔离。 因此,落在该范围内的任何值都将共享相同的 id,即使在 REPL 中也是如此:

>>> a = 100
>>> b = 100
>>> a is b
True

自 300 > 256 年以来,它没有被拘留:

>>> a = 300
>>> b = 300
>>> a is b
False

其次,在脚本中,文字被放入 编译后的代码。Python 足够聪明,可以意识到,由于两者都引用了文字,而这是一个不可变的对象,因此它可以 继续并引用相同的常量位置。如果你调整你的脚本 一点,写成:ab300300

def foo():
    a = 300
    b = 300
    print(a==b)
    print(a is b)
    print("id(a) = %d, id(b) = %d" % (id(a), id(b)))


import dis
dis.disassemble(foo.__code__)

输出的开始部分如下所示:

2           0 LOAD_CONST               1 (300)
            2 STORE_FAST               0 (a)

3           4 LOAD_CONST               1 (300)
            6 STORE_FAST               1 (b)

...

正如你所看到的,CPython 正在加载并使用相同的常量槽。 这意味着 和 现在引用同一个对象(因为它们 引用相同的插槽),这就是为什么在脚本中但 不在 REPL。ababa is bTrue

如果将语句包装在函数中,也可以在 REPL 中看到此行为:

>>> import dis
>>> def foo():
...   a = 300
...   b = 300
...   print(a==b)
...   print(a is b)
...   print("id(a) = %d, id(b) = %d" % (id(a), id(b)))
...
>>> foo()
True
True
id(a) = 4369383056, id(b) = 4369383056
>>> dis.disassemble(foo.__code__)
  2           0 LOAD_CONST               1 (300)
              2 STORE_FAST               0 (a)

  3           4 LOAD_CONST               1 (300)
              6 STORE_FAST               1 (b)
# snipped...

一句话:虽然 CPython 有时会进行这些优化,但你不应该真的指望它——它实际上是一个实现细节,并且随着时间的推移而发生了变化(例如,CPython 过去只对 100 以内的整数这样做)。如果要比较数字,请使用 .:-)==

评论

0赞 wim 3/26/2019
这个答案还有很多不足之处:通过“调整脚本”,您将代码放入函数作用域(局部变量)而不是模块作用域(全局变量)中,就执行和名称解析而言,模块作用域现在是一个完全不同的范围情况。我认为这在某种程度上使拆卸无效。是的,碰巧在模块代码上使用相同的窥视孔优化器,就像在函数体上使用一样,但事实并非如此。答案也没有真正解释为什么没有直接在 REPL 上进行相同的优化。
0赞 John Szakmeister 3/27/2019
@wim 如果您认为答案不符合标准,我肯定会删除它。我个人并不觉得这种调整是负面的,但我听到了你的声音——这并不完全相同。但我认为所有这些都与 Python 今天所做的事情密切相关,并不能保证 Python 将来会做什么。这是一个公平的批评——我没有明确说明为什么 REPL 的逐行是不同的。
0赞 wim 3/27/2019
哦,我认为你不应该删除它,只需更新它!我确实认为拆卸演示是有用的内容。但是,它应该模块范围的代码,而不是函数范围的代码 - 最新版本的 dis 允许您直接传入源字符串,因此没有必要使用函数对象的属性。dis__code__
0赞 John Szakmeister 3/28/2019
@wim 这对你来说似乎不太公平——我认为你已经在这方面做了工作,你的答案应该是公认的答案(尽管我认为在答案中加入小整数的实习是好的,以防万一人们试图得到他们意想不到的结果)。