IS 运算符在 python 中的不常见行为



从 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

为什么?

你看到的是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 不同,但不同的行为是我的错,因为我的代码依赖于特定于实现的行为。

一般来说,-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
>>> 

来自 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正在做的优化类型,恐怕你需要在它的源代码中挖掘出来。

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

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

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

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

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

很明显,无论Python在"一个块"中采用什么,都是不确定的,可以根据它的编写方式,单行与否以及使用的版本,操作系统和发行版来改变。

最新更新