“是”运算符的行为意外与非caching整数

在和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在函数func声明时指向同一个int实例,但是相反,它们指向不在其外的对象。

这是为什么?


注意 :我了解了Python的“is”运算符中所描述的identity( is )和equality( == )运算之间的区别。 另外,我也意识到python正在执行的范围[-5, 256]的整数的caching,正如“is”中所描述的那样,整个操作符的行为意外 。

不是这种情况,因为这些数字超出了这个范围, 想要评估身份而不是平等。

TL;博士:

如参考手册所述:

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

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

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

需要注意的是,这种行为并不仅仅以int文字来显示,你会得到类似的结果,例如float文字(见这里 )。

当然,比较对象应该(除了明确的is Nonetesting)应该总是用相等运算符==来完成,而不是

这里所说的一切都适用于Python,CPython的最stream行的实现。 其他实现可能有所不同,因此在使用它们时不应该做出任何假设。


更长的答案:

为了得到一个更清晰的视图,另外validation这个看似奇怪的行为,我们可以使用dis模块直接查看这些情况下的code对象。

对于函数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 

我们只对函数funcConstants条目感兴趣。 其中,我们可以看到,我们有两个值, None (总是存在)和1000 。 我们只有一个表示常量1000 int实例。 这是调用函数时将要分配给ab的值。

访问这个值很容易通过func.__code__.co_consts[1]等等,另一种方式来查看我们的a is b在函数中的评估就像这样:

 >>> id(func.__code__.co_consts[1]) == id(func.__code__.co_consts[1]) 

当然,因为我们指的是同一个对象,所以它会评价为“ True

对于每个交互式命令:

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

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

 >>> com1 = compile("a=1000", filename="", mode="exec") >>> com2 = compile("b=1000", filename="", mode="exec") 

对于每个赋值语句,我们将得到一个类似的代码对象,如下所示:

 >>> 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 。 这就是为什么在这种情况下,当我们通过co_consts参数做a is b ,我们实际上得到:

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

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

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


注意:在源代码中发生了这样的事情,并且仔细研究之后,我有点好奇,我相信我终于find了它。

在编译阶段, 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字符的对象会被创build,从而得到一个True值。

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

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

    …以下是块: 一个模块

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

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

    这意味着,除非我们明确初始化为同一个可变对象(例如a = b = []),否则这些对象的身份永远不会相等,例如:

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

    再次,在文档中指定:

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

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

compiler_add_o()函数中,你会看到在添加一个新常量(并增加索引)之前,会检查字典是否存在常量对象和索引。 如果是这样,他们被重用。

总之,这意味着一个语句中的重复常量(比如在你的函数定义中)被折叠成一个单例。 相反,你的a = 1000b = 1000是两个单独的陈述,所以不会发生折叠。

FWIW,这只是一个CPython的实现细节(即不受语言保证)。 这就是为什么在这里给出的参考是C源代码,而不是语言规范,不作任何保证的主题。

希望你对CPython的工作原理有如此的了解:-)

Interesting Posts