`is`运算符在非缓存整数上的行为不可预测。

44

当我在使用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()函数求值,这意味着在函数func中声明的ab指向同一个int实例,但在函数外部声明时则指向不同的对象。

为什���会这样呢?


注意:我知道身份(is)和相等性(==)操作之间的区别,如了解Python的"is"运算符中所述。此外,我也知道Python对范围为[-5, 256]内整数的缓存处理,如"is"运算符对整数的行为中所述。

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


4
Python语言的定义保证了单例对象None、False和True是它们本身,并且多个可变内置类的实例并不不同。多个不可变内置类实例具有相同值的“isness”取决于值、版本和实现。我假设“Python解释器”指的是CPython。使用其他解释器可能会得到不同的结果。“小”整数值(如1000)在CPython中的结果与使用250等较小值的结果不同。对于旧版本的CPython,你可能会得到不同的结果。 - Terry Jan Reedy
这对你有什么吸引力?在整数上使用 is 感觉不太对。 - Martin Bonner supports Monica
6
我很感兴趣的是CPython如何实现。我遇到了这种行为,对其进行了检查,并决定发布一个问答帖子,认为其他人也可能会觉得有趣。虽然这种方式不正确,但我并不建议使用它;-) - Dimitris Fasarakis Hilliard
2个回答

66

简而言之:

正如参考手册所述:

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

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

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

请注意,这种行为不仅适用于int字面量,例如float字面量也会得到类似的结果(请参见此处)。
当然,比较对象(除了显式的is None测试)应该始终使用等号运算符==而不是is这里提到的所有内容都适用于Python的最流行实现CPython。其他实现可能有所不同,因此在使用它们时不应做出任何假设。

较长的回答:

为了更清晰地了解和验证这种看似奇怪的行为,我们可以直接查看每种情况下的代码对象,并使用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

我们只关心函数funcConstants条目。在其中,我们可以看到有两个值:None(始终存在)和1000。我们只有一个代表常量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中的更多信息。

注意事项:

  • Chained statements will evaluate to an identity check of True

    It should be more clear now why exactly the following evaluates to True:

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

    In this case, by chaining the two assignment commands together we tell the interpreter to compile these together. As in the case for the function object, only one object for the literal 1000 will be created resulting in a True value when evaluated.

  • Execution on a module level yields True again:

    As previously mentioned, the reference manual states that:

    ... The following are blocks: a module ...

    So the same premise applies: we will have a single code object (for the module) and so, as a result, single values stored for each different literal.

  • The same doesn't apply for mutable objects:

意思是,除非我们明确地将可变对象初始化为相同的对象,否则这些对象的身份永远不会相等,例如:a = b = [],这样做才能使它们的身份相等。
    a = []; b = []
    a is b  # always evaluates to False

文档中再次指定:

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


20
在交互提示符中,输入是以单个模式处理一条完整语句的(已编译)。编译器本身(在Python/compile.c中)跟踪常量,使用称为u_consts的字典将常量对象映射到其索引。
compiler_add_o()函数中,您可以看到在添加新常量(并增加索引)之前,检查字典以查看常量对象和索引是否已经存在。如果是,则重复使用它们。
简而言之,这意味着一条语句中重复出现的常量(例如在函数定义中)被折叠成一个单例。相反,您的a = 1000b = 1000是两个独立的语句,因此不会进行折叠。

FWIW,这仅仅是CPython的一个实现细节(即不受语言保证)。这就是为什么这里给出的参考文献是C源代码而不是语言规范,后者对此不做任何保证。

希望您喜欢这个了解CPython底层工作的视角 :-)


谢谢,需要一份权威的答案来验证我所写的内容(我也需要一个奖励赏金的地方 :-))。 - Dimitris Fasarakis Hilliard
3
@Jim 很高兴能帮忙。偶尔会有Python核心开发人员在StackOverflow上潜伏,并且可以为您解决问题的关键。 - Raymond Hettinger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接