在使用Python解释器时,我偶然发现了关于is
运算符的这个相互矛盾的情况:
如果评估发生在函数中,则返回True
,如果在外部完成,则返回False
。
>>> def func():
... a = 1000
... b = 1000
... return a is b
...
>>> a = 1000
>>> b = 1000
>>> a is b, func()
(False, True)
由于is
运算符为所涉及的对象评估id()
,这意味着a
和b
在函数int
内部声明时指向相同的func
实例,但相反,它们在其外部指向不同的对象。
为什么会这样?
注意:我知道在is
中描述的identity(==
)和相等(Understanding Python's "is" operator)操作之间的区别。另外,我也知道python正在[-5, 256]
中描述的范围"is" operator behaves unexpectedly with integers中的整数执行缓存。
这不是这里的情况,因为数字超出了该范围,我确实想要评估身份而不是平等。
正如reference manual所述:
块是一段Python程序文本,作为一个单元执行。以下是块:模块,函数体和类定义。交互式输入的每个命令都是一个块。
这就是为什么,在函数的情况下,你有一个代码块,其中包含数字文字1000
的单个对象,所以id(a) == id(b)
将产生True
。
在第二种情况下,你有两个不同的代码对象,每个代码对象都有自己的文字1000
不同的对象,所以id(a) != id(b)
。
请注意,此行为不会仅与int
文字一起显示,您将获得类似的结果,例如,float
文字(请参阅here)。
当然,比较对象(显式is None
测试除外)应该始终使用等于运算符==
而不是is
。
这里陈述的所有内容都适用于最流行的Python CPython实现。其他实现可能不同,因此在使用它们时不应进行任何假设。
为了获得更清晰的视图并另外验证这种看似奇怪的行为,我们可以使用code
模块直接在dis
对象中查找每种情况。
对于函数func
:
除了所有其他属性外,函数对象还具有__code__
属性,允许您查看该函数的已编译字节码。使用dis.code_info
,我们可以获得给定函数的代码对象中所有存储属性的漂亮视图:
>>> print(dis.code_info(func))
Name: func
Filename: <stdin>
Argument count: 0
Kw-only arguments: 0
Number of locals: 2
Stack size: 2
Flags: OPTIMIZED, NEWLOCALS, NOFREE
Constants:
0: None
1: 1000
Variable names:
0: a
1: b
我们只对功能Constants
的func
条目感兴趣。在其中,我们可以看到我们有两个值,None
(总是存在)和1000
。我们只有一个表示常量1000
的int实例。这是调用函数时将分配a
和b
的值。
通过qazxsw poi可以很容易地访问这个值,所以,在函数中查看你的qazxsw poi评估的另一种方法是这样的:
func.__code__.co_consts[1]
当然,这将评估a is b
,因为我们指的是同一个对象。
对于每个交互命令:
如前所述,每个交互式命令都被解释为单个代码块:独立解析,编译和评估。
我们可以通过内置的>>> id(func.__code__.co_consts[1]) == id(func.__code__.co_consts[1])
获取每个命令的代码对象:
True
对于每个赋值语句,我们将得到一个类似的代码对象,如下所示:
compile
>>> com1 = compile("a=1000", filename="", mode="single")
>>> com2 = compile("b=1000", filename="", mode="single")
的相同命令看起来相同,但有一个根本区别:每个代码对象>>> print(dis.code_info(com1))
Name: <module>
Filename:
Argument count: 0
Kw-only arguments: 0
Number of locals: 0
Stack size: 1
Flags: NOFREE
Constants:
0: 1000
1: None
Names:
0: a
和com2
都有不同的int实例,代表文字com1
。这就是为什么,在这种情况下,当我们通过com2
论证做1000
时,我们实际得到:
a is b
这与我们实际得到的结果一致。
不同的代码对象,不同的内容。
注意:我对源代码中究竟是如何发生这种情况有点好奇,经过挖掘后我相信我终于找到了它。
在编译阶段,co_consts
属性由字典对象表示。在>>> id(com1.co_consts[0]) == id(com2.co_consts[0])
False
中,我们实际上可以看到初始化:
co_consts
在编译期间,检查已存在的常量。有关详细信息,请参阅compile.c
。
/* snippet for brevity */
u->u_lineno = 0;
u->u_col_offset = 0;
u->u_lineno_set = 0;
u->u_consts = PyDict_New();
/* snippet for brevity */
的身份检查
现在应该更清楚为什么以下评估@Raymond Hettinger's answer below:
True
在这种情况下,通过将两个赋值命令链接在一起,我们告诉解释器将它们一起编译。与函数对象的情况一样,只会创建文字True
的一个对象,在计算时会生成>>> a = 1000; b = 1000;
>>> a is b
值。1000
:
如前所述,参考手册指出:
......以下是块:模块......
因此同样的前提适用:我们将有一个代码对象(对于模块),因此,为每个不同的文字存储单个值。True
再次,在True
中,这是指定的:
在a = 1之后; b = 1,a和b可能会或可能不会引用具有值1的同一对象,具体取决于实现,但在c = []之后; d = [],c和d保证引用两个不同的,唯一的,新创建的空列表。在交互式提示符下,条目是a = []; b = []
a is b # always returns false
,它一次处理一个完整的语句。编译器本身(在the documentation中)跟踪名为compiled in a single mode的字典中的常量,该常量将常量对象映射到其索引。
在Python/compile.c函数中,您会看到在添加新常量(并递增索引)之前,将检查dict以查看常量对象和索引是否已存在。如果是这样,它们将被重用。
简而言之,这意味着一个语句中的重复常量(例如在函数定义中)被折叠成一个单例。相比之下,你的u_consts和compiler_add_o()是两个单独的陈述,因此不会发生折叠。
FWIW,这只是一个CPython实现细节(即语言无法保证)。这就是为什么这里给出的引用是C源代码而不是语言规范,它不能保证主题。
希望您喜欢CPython如何在引擎盖下工作的洞察力:-)