在使用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
外部声明时指向不同的对象。为什么会这样?
注意:我知道在Understanding Python's "is" operator中描述的identity(
==
)和equality([-5, 256]
)操作之间的区别此外,我还知道python正在为这里不是这样的,因为数字在这个范围之外,我确实想评估身份,而不是平等。
最佳答案
TL;博士:
正如reference manual所述:
块是作为一个单元执行的一段python程序文本。
以下是块:模块、函数体和类定义。
交互输入的每个命令都是一个块。
这就是为什么在函数的情况下,只有一个代码块,其中包含数字文本的单个对象1000
,因此id(a) == id(b)
将产生True
。
在第二种情况下,您有两个不同的代码对象,每个对象都有自己的文本1000
soid(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
我们只有一个int实例表示常量1000
。这是调用函数时要分配给a
和b
的值。通过
func.__code__.co_consts[1]
访问此值很容易,因此,在函数中查看a is b
评估的另一种方法如下:>>> id(func.__code__.co_consts[1]) == id(func.__code__.co_consts[1])
当然,因为我们指的是同一个对象,所以它的计算结果会小于cc>。
对于每个交互命令:
如前所述,每个交互命令被解释为一个单独的代码块:独立地解析、编译和计算。
我们可以通过内置的
True
获取每个命令的代码对象:>>> 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
相同的
compile
命令看起来相同,但有一个根本的区别:每个代码对象com2
和com1
都有不同的int实例表示文本com2
。这就是为什么,在这种情况下,当我们通过1000
参数a is b
时,我们实际上得到:>>> id(com1.co_consts[0]) == id(com2.co_consts[0])
False
这与我们实际得到的一致。
不同的代码对象,不同的内容。
注意:我有点好奇,这到底是怎么发生在源代码中,经过挖掘,我相信我终于找到了它。
在编译阶段,
co_consts
属性由dictionary对象表示。在co_consts
中,我们可以看到初始化:/* 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了解更多信息。
注意事项:
链式语句将计算为
compile.c
现在应该更清楚的是,为什么下面的计算结果是
True
:>>> a = 1000; b = 1000;
>>> a is b
在这种情况下,通过将两个赋值命令链接在一起,我们告诉解释器将它们编译在一起。与函数对象的情况一样,只有一个文本
True
的对象将被创建,从而在计算时产生1000
值。在模块级执行会再次产生
True
:如前所述,参考手册规定:
... 以下是块:一个模块。。。
因此,同样的前提也适用:我们将有一个单独的代码对象(对于模块),因此,每个不同的文本存储一个值。
这同样不适用于可变对象:
也就是说,除非我们显式初始化为同一个可变对象(例如a=b=[]),否则对象的标识永远不会相等,例如:
a = []; b = []
a is b # always returns false
同样,在the documentation中,这是指定的:
在a=1;b=1之后,a和b可能引用值为1的同一个对象,也可能不引用值为1的同一个对象,具体取决于实现,但是在c=[];d=[]之后,c和d保证引用两个不同的、唯一的、新创建的空列表。
关于python - “is”运算符对非缓存整数的行为异常,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51259003/