Python 精讲 | 奇葩的 is
大家好,欢迎来到 Crossin的编程教室 !
接下来的几个例子,可能会颠覆你对 Python 的认知。
我们知道,Python 判断两个数值是否相等的运算符是「==」。比如有一个变量 a 是整数 1,另一个变量 b 是小数 1.0,尽管它们类型不同,但代表的数值是相等的,所以 a == b 结果是 True。
Python 中还有一个运算符 is,它用来判断两个对象是否相同。
一个是相等,一个是相同,虽然只差一个字,但 is 却没有那么简单。
我们打开一个 Python 交互环境,在里面定义一个变量 a = 1.0,再定义一个变量 b = a。
a is b 的结果是 True,这个还算好理解,因为 b 就是 a 嘛。
如果 b 不是由 a 赋值,而是直接赋值为 1.0。这时 a is b 的结果就是 False。这个也可以理解,虽然值相等,但它们是两个变量,并不相同。
不过接下来,情况就开始变得复杂了。
你要说分别赋值的变量就是不相同,那我们把赋给变量的值,从 1.0 改成 1,结果就又成了 True。
难道是因为浮点数和整数类型的原因吗?
那我们再把值从 1,改成 1000,同样是整数,结果却是 False。
然而还没完,我们把同样的代码写在一个 py 文件中运行,结果就是 True。
但也不全是 True。如果这两个变量不在一个作用域,就是 False。
你可能要说,不同作用域的变量肯定不相同嘛,但如果值改回为 1,又成了 True。
前面的例子都是直接赋值,那如果加入计算会怎样?
我们在让 b 在 a 的基础上加上 0,b 的值完全没有变化,结果却从 True 变成了 False。
但再换个计算式,又是 True
这到底是怎么回事呢?
背后的原因其实是 Python 解释器的三个优化操作。首先,是
1. 小整数池
Python 为了优化速度,在每次执行代码时,会提前把 -5 到 256 的整数创建好。因为这些小整数是会被经常用到的。而当你创建一个值在这个范围内的整数时,就不是临时再去创建一个对象,而是直接指向已经建好的对象。所以不管你有多少个变量,实际都是同一个对象。
我们可以用id函数来验证这一点:
而对于小数没有这样的优化,因为小数实在太多了。大于 256 的整数也没有。
那为什么写在 py 文件里的大整数就是相同的呢?这就要说到 Python 另一个优化:
2. 大整数缓存
尽管大于 256 的整数不会提前创建好,但如果 Python 解释器发现你用到重复的整数常量,也会将后面的变量指向已经创建好的对象。
所以不仅是在 py 文件中,即使在交互环境下,如果把两个大整数的赋值写在同一行,或者放在一个代码块中,也会发现它们是相同的。
但这种优化仅限于数值常量,对于带有变量的计算就不起作用了,因为 Python 无法提前预判变量的值。
而对于不带变量的纯数值计算,Python 又做了一次优化:
3. 常量折叠
Python 在编译阶段会把常量表达式计算成结果并替换。所以不管你是 10 * 100 还是 10 ** 3 又或是 111 * 9 + 1,对 Python 来说都是 1000,于是也同样被缓存了。
以上这些,就是 is 会呈现出看似混乱结果的原因。但请注意,这些解释器的优化,并不是 Python 语言层面的特性。它们可能因环境和版本的不同而产生不同的结果。
比如在 Python 3.7 中,不同作用域的大整数不会被缓存为同一个对象,但在 Python 3.11 中,却是相同的。
作为开发者来说,最好的选择就是不要在比较数值相等时使用 is。只有一种情况建议用 is 而不是 ==,那就是判断一个值是否为空值 None。
你知道是什么原因吗?欢迎留言区讨论。