“是”运营商,非缓存整数意外行为“是”运营商,非缓存整数意外行为('is' oper

2019-06-17 10:13发布

当与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 )和平等( == )操作中的说明了解Python的“是”经营者 。 此外,我也知道关于由蟒蛇范围内正在执行对整数缓存[-5, 256]如在描述“是”经营者与整数意外行为 。

不是这里的情况 ,因为数字是范围之外, 我也想评估的身份,而不是平等。

Answer 1:

TL;博士:

作为参考手册中指出:

A嵌段是作为一个单元执行的片Python程序文本。 以下是块:一个模块,功能体,和一个类定义。 每条交互式输入的命令是一个块。

这就是为什么,在功能的情况下,你只有一个代码块包含了数字文本的单个对象1000 ,所以id(a) == id(b)将产生True

在第二种情况下,你有两个不同的代码对象各有自己不同的对象字面1000所以id(a) != id(b)

需注意,此行为不会表现int文字而已,你会得到类似的结果,例如, float文字(见这里 )。

当然,比较对象(除明确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 。 这是值ab将要分配在调用函数时。

访问该值是通过容易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看起来相同,但有一个根本的区别 :每个代码的对象com1com2有代表字面不同INT实例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的回答多一点这一点。


注意事项:

  • 链式语句将评估到的身份检查True

    它应该是比较清楚现在到底为什么下面的计算结果为True

     >>> a = 1000; b = 1000; >>> a is b 

    在这种情况下,通过链接这两个任务命令我们一起告诉解释编译这些在一起 。 作为在用于功能对象的情况下,仅一个用于文本对象1000将被创建导致True值来评价时。

  • 在模块级执行产生True一次:

    如前面提到的,参考手册指出:

    ...以下是代码块: 模块 ...

    因此,相同的前提下适用:我们将(用于模块)一个代码的对象,因此,作为结果,存储每个不同的字面单值。

  • 同样不适用于可变对象:

    这意味着除非我们明确地初始化到相同的可变对象(例如具有A = B = [])中,对象的标识将永远不会是相等的,例如:

     a = []; b = [] a is b # always returns false 

    再次,在文档 ,这是规定:

    后一个= 1; B = 1,a和b可以或可以不指代相同的对象与所述值之一,取决于实现,但℃后= []; d = [],c和d被保证是指两个不同的,唯一的,新创建的空列表。



Answer 2:

在互动提示下,进入被编在一个单一的模式,即一次处理一个完整的陈述。 编译器本身(在Python的/ compile.c )跟踪对字典称为常量u_consts使得恒定对象映射到其索引。

compiler_add_o()函数,可以看到之前添加一个新的常数(和递增索引),该字典进行检查,看定目标和指标是否已经存在。 如果是这样,他们被重用。

总之,这意味着在一个声明中反复常量(如函数定义)被折叠成一个单例。 与此相反,您的a = 1000b = 1000是两个独立的语句,所以没有折叠而发生。

FWIW,这一切只是一个CPython的实现细节(即不是由语言保证)。 这就是为什么在这里给出的参考是到C源代码,而不是语言规范,这使得关于这个问题的任何保证。

希望你喜欢这个洞察CPython的引擎盖下是如何工作:-)



文章来源: 'is' operator behaves unexpectedly with non-cached integers