'是'运算符与非缓存的整数意外地行为

时间:2015-12-08 03:33:41

标签: 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内部声明的实例,但相反,它们指向外部的不同对象。

为什么会这样?

注意:我知道Understanding Python's "is" operator中描述的身份(is)和平等(==)操作之间的区别。另外,我也知道python对范围[-5, 256]中的整数执行的缓存,如"is" operator behaves unexpectedly with integers中所述。

这个不是这种情况,因为数字超出了这个范围而我确实想要评估身份和相等。 功能

2 个答案:

答案 0 :(得分:61)

TL; DR:

正如reference manual所述:

  

块是一段Python程序文本,作为一个单元执行。   以下是块:模块,函数体和类定义。   以交互方式输入的每个命令都是一个块。

这就是为什么,在函数的情况下,你有一个单个代码块,其中包含数字文字的单个对象 1000id(a) == id(b)将产生True

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

请注意,此行为不会仅显示int个文字,您会得到类似的结果,例如float文字(请参阅here)。

当然,应该始终使用等于运算符is None ==来比较对象(显式is测试除外)。

这里陈述的所有内容都适用于最流行的Python CPython实现。其他实现可能会有所不同,因此在使用它们时不应进行任何假设。

更长的答案:

为了获得更清晰的视图并另外验证此看似奇怪的行为,我们可以使用code模块直接查看每个案例的dis对象。< / p>

对于函数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的{​​{1}}条目感兴趣。在其中,我们可以看到我们有两个值func(始终存在)和None。我们只有一个单个 int实例,表示常量1000。这是调用函数时将1000a分配给的值。

通过b轻松访问此值,因此,在函数中查看func.__code__.co_consts[1]评估的另一种方法是:

a is b

当然,这将评估为>>> id(func.__code__.co_consts[1]) == id(func.__code__.co_consts[1]) ,因为我们指的是同一个对象。

对于每个互动命令:

如前所述,每个交互式命令都被解释为单个代码块:独立解析,编译和评估。

我们可以通过compile内置函数获取每个命令的代码对象:

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 的相同命令看起来相同,但有根本区别:每个代码对象com2com1都有不同的int实例,代表文字com2。这就是为什么在这种情况下,当我们通过1000参数a is b时,我们实际得到:

co_consts

这与我们实际得到的结果一致。

不同的代码对象,不同的内容。

注意:我对源代码中究竟是如何发生这种情况有点好奇,在深入了解之后我相信我终于找到了它。

在编译阶段,co_consts属性由字典对象表示。在compile.c我们实际上可以看到初始化:

>>> id(com1.co_consts[0]) == id(com2.co_consts[0])
False

在编译期间,检查已存在的常量。有关详细信息,请参阅@Raymond Hettinger's answer below

注意事项:

  • 链式语句将评估为/* 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 */

    的身份检查

    现在应该更清楚为什么以下评估结果为True

    True

    在这种情况下,通过将两个赋值命令链接在一起,我们告诉解释器将这些编译在一起。与函数对象的情况一样,只有一个文字>>> a = 1000; b = 1000; >>> a is b 的对象将被创建,在评估时会产生1000值。

  • 在模块级别执行会再次产生True

    如前所述,参考手册指出:

      

    ......以下是块:模块 ...

    所以同样的前提适用:我们将有一个代码对象(对于模块),因此,为每个不同的文字存储单个值。

  • 适用可变对象:

    这意味着除非我们显式初始化为相同的可变对象(例如,使用a = b = []),否则对象的标识永远不会相等,例如:

    True

    同样,在the documentation中,指定了:

      a = 1后

    ; b = 1,a和b可能会或可能不会引用具有值1的同一对象,具体取决于实现,但在c = []之后; d = [],c和d保证引用两个不同的,唯一的,新创建的空列表。

答案 1 :(得分:18)

在交互式提示符下,条目为compiled in a single mode,一次处理一个完整的语句。编译器本身(在Python/compile.c中)跟踪名为u_consts的字典中的常量,该常量将常量对象映射到其索引。

compiler_add_o()函数中,您会看到在添加新常量(并递增索引)之前,会检查dict以查看常量对象和索引是否已存在。如果是这样,它们将被重用。

简而言之,这意味着一个语句中的重复常量(例如在函数定义中)被折叠成一个单例。相比之下,您的a = 1000b = 1000是两个单独的陈述,因此不会发生折叠。

FWIW,这只是一个CPython实现细节(即语言无法保证)。这就是为什么这里给出的引用是C源代码而不是语言规范,不能保证主题。

希望您喜欢CPython如何在幕后工作的洞察力: - )