【问题标题】:The `is` operator behaves unexpectedly with non-cached integers`is` 运算符对非缓存整数的行为异常
【发布时间】:2016-03-12 21:05:34
【问题描述】:

在使用 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 实例,但是,在相反,它们在外部指向不同的对象。

为什么会这样?


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

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

【问题讨论】:

  • Python 语言的定义保证了单例 None、False 和 True 是它们自己,并且可变 bultin 类的多个实例没有区别。具有相同值的不可变内置类的多个实例的存在性取决于值、版本和实现,我认为“Python 解释器”是指 CPython。使用其他口译员可能会得到不同的结果。对于“小”int 值,您将使用 CPython 获得不同的结果。尝试 250 而不是 1000。对于旧版本的 CPython,您可能会得到不同的结果。
  • 您为什么对此感兴趣?在整数上使用is 对我来说是错误的。
  • @MartinBonner 我主要对 CPython 的实现方式感兴趣。我遇到了这种行为,对其进行了检查,并决定发布一个问答,认为其他人可能也会觉得它很有趣。这是错误的,我不建议使用它;-)

标签: python python-3.x integer identity python-internals


【解决方案1】:

tl;博士:

正如reference manual 所说:

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

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

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

请注意,这种行为不会仅在 int 文字中体现,您将获得类似的结果,例如 float 文字(请参阅 here)。

当然,比较对象(显式 is None 测试除外)应始终使用相等运算符 ==not 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。我们只有一个 single 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。这就是为什么在这种情况下,当我们通过 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's answer below 了解更多信息。


注意事项:

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

    现在应该更清楚为什么下面的结果是True

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

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

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

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

    ... 以下是块:一个模块 ...

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

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

意思是除非我们显式初始化为同一个可变对象(例如a = b = []),否则对象的身份永远不会相等,例如:

    a = []; b = []
    a is b  # always evaluates to False

同样,在the documentation 中,这是指定的:

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

【讨论】:

    【解决方案2】:

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

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

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

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

    希望您喜欢这篇关于 CPython 底层工作原理的见解 :-)

    【讨论】:

    • 谢谢,一个权威的答案来验证我在写什么是非常需要的(我也需要一个地方来奖励赏金:-)
    • @Jim 乐于助人。有时,一位 Python 核心开发人员碰巧潜伏在 StackOverflow 上,可以将您带入问题的核心。
    猜你喜欢
    • 2010-09-23
    • 2022-10-07
    • 2016-12-14
    相关资源
    最近更新 更多