'是'操作员意外地表现为非

在与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]的整数的缓存,如“is”中所描述的那样,整数运算符意外地行为。

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


TL;博士:

如参考手册所述:

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

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

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

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

当然,比较对象(除明确is None测试)应始终与平等的运营商做== ,而不是is

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


较长答案:

为了获得更清晰的视图并且另外验证这个看似奇怪的行为,我们可以使用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 。 我们只有表示常数单个 int实例1000 。 这是调用函数时ab将要分配的值。

通过func.__code__.co_consts[1]访问这个值很容易,所以,在函数中查看我们的a is b评估的另一种方式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 。 这就是为什么在这种情况下,当我们通过co_consts参数做a is b ,我们实际上得到:

>>> 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
    

    再次,在文档中,这是指定的:

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


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

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

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

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

    希望您对CPython如何在引擎盖下工作深有体会:-)

    链接地址: http://www.djcxy.com/p/58611.html

    上一篇: 'is' operator behaves unexpectedly with non

    下一篇: What's the difference between equal?, eql?, ===, and ==?