python - “is”运算符对非缓存整数的行为异常

标签 python python-3.x int identity python-internals

在使用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(),这意味着ab在函数int内部声明时指向同一个func实例,但在函数is外部声明时指向不同的对象。
为什么会这样?
注意:我知道在Understanding Python's "is" operator中描述的identity(==)和equality([-5, 256])操作之间的区别此外,我还知道python正在为范围内的整数执行缓存,如"is" operator behaves unexpectedly with integers中所述。
这里不是这样的,因为数字在这个范围之外,我确实想评估身份,而不是平等。

最佳答案

TL;博士:
正如reference manual所述:
块是作为一个单元执行的一段python程序文本。
以下是块:模块、函数体和类定义。
交互输入的每个命令都是一个块。
这就是为什么在函数的情况下,只有一个代码块,其中包含数字文本的单个对象
1000,因此id(a) == id(b)将产生True
在第二种情况下,您有两个不同的代码对象,每个对象都有自己的文本1000soid(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

我们只对函数Constantsfunc项感兴趣。在它中,我们可以看到有两个值,None(始终存在)和1000我们只有一个int实例表示常量1000。这是调用函数时要分配给ab的值。
通过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命令看起来相同,但有一个根本的区别:每个代码对象com2com1都有不同的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/

相关文章:

在c中无法将int初始化为0

c# - 获取查询的 int 值

python - python unicode 中的正则表达式

python - 当catplot图是子图的一部分时,如何修改它的 "ylabel"属性?

python - 如何在 Python 中使用正则表达式删除带有特殊字符串的字符?

python - 如何使用空格作为分隔符来获取字符串中的子字符串?

python-3.x - 输入 lxml 的提示?

java - 为什么 RandomAccessFile 使用 int 作为偏移量

python - 如何使用 Python 实现并行 gzip 压缩?

python - 在 Python 中从列表创建字典 - 字典键忽略列表中的相同项目