小心Python中的“is”
By 青衣极客 Blue Geek In 2020-04-16
Python 的很多语法用起来都非常自然,比如“is”,但与这个“is”相关的语法也有一些坑。特别是在与字符串一起使用时,由于解释器的“驻留机制”,常常造成一些匪夷所思的现象。 虽然也并不会造成特别严重的事故,但知晓其中的原理之后,确实能够避免一些不必要的“踩坑”经历。
1. 一个众说纷纭的案例
“is”与字符串联合使用的坑得从一段 众说纷纭的程序运行结果 说起。首先,我们来看一段非常合理的程序
第一段程序
a = "some_string"
b = "some" + "_" + "string"
print(a is b)
大家可以想一想,这段程序会输出什么呢?如果你是解释器的设计者,你会不会认为 a is b 应该是 True 呢?有一些人认为不应该,因为这毕竟是两个对象啊;而我们大部分人都认为是应该的,毕竟确实是一致,这一点并不奇怪。 事实上,Python解释器也是这么做的,即以上代码在 Python3.7 的解释器下获得的结果是 “True”。这里之所以要强调 Python 的版本,是因为无法保证不同版本的解释器能得到相同的结果,并且以下的运行结果都是在 Python3.7 中运行的。
那么下面一段代码是不是也会输出 “True” 呢?
第二段程序
c = "hello"
d = "hello"
print(c is d)
如果我们认为第一段代码应该输出 “True” 的话,那么第二段代码输出 “True” 就是自然而然的事情了。我们在 Python3.7 中实验也确实得到我们想要的结果 “True”。到这里似乎都很正常,也没有什么所谓的“坑”,但是,看了下面的一段代码,可能你的世界观就坍塌了。
第三段程序
e = "hello!"
f = "hello!"
print(e is f)
沿用 a is b 和 c is d 都是 “True” 的实验结果,我们认为 e is f 肯定也必须是 “True”吧?但实际情况让我们大跌眼镜,第三段程序居然在 Python3.7 中输入 “False”。 到这里,我们就疑惑了,第二段与第三段程序的差别只有一点,即第三段的字符串多了 “!” 号,难道 Python 解释器语法与数据的内容有关? 如果没法洞悉其中的秘密,我们先记下这个结果,再看第四段程序。
第四段程序
g,h = "hello!", "hello!"
print(g is h)
按照第三段程序的逻辑,第四段程序应该输出 “False” 吧?在 Python3.7 中确实如此,输出了 “False”。但是这个事情并没有结束,因为有人在 Python3.8 中跑出的结果是 “True”。 到这里,我们不得不冷静下来想一想,这些语句之间的区别和联系,也不得不想一想 Python 中 “is” 关键字的实现原理。
2. “is”的原理
我们常常使用 if x is True 或者 if y is False 类似这样的语句来进行条件判断,我们以为 “is” 是通过判断两个变量或者变量与常量的内容是否一致来实现的,但是事实上并非如此。“is” 是通过判断对象的对象的逻辑地址是否一致来工作的。
对 Python 稍微了解一些的朋友大概都知道,我们没有办法像 “C/C++” 那样直接修改内存地址指向的内容,但这并不能说明 Python 中没有地址。实际上,Python 的每一个对象都有对应的 “逻辑地址”。 这个地址可以使用 id() 函数传入对象获取,但是 该地址的用处仅仅是判断对象是否相同,而没有其他用途。
“is” 关键字,正是通过 id() 函数获取到对象的 “逻辑地址”, 如果地址一致,则返回 “True”;不一致,则返回 “False”。我们可以使用 id() 函数一一打印上面4段程序中的8个字符串对象,发现,“is” 的判断结果为 “True” 的一组对象的 “逻辑地址” 相同。

也就是说,上面四段程序中,在 Python3.7里面,“a-b”是在同一个地址,“c-d”也是在同一个地址;而”e-f”的地址不相同,”g-h”的地址也不相同。这就有点诡异呢?第一,Python为什么要把不同对象放在同一个地址呢?为什么又不是所有具有相同内容的对象都放到同一个地址呢? 关于这些疑惑就需要 “驻留机制” 来解释了。
3. 驻留机制
所谓 “驻留机制” 是指,解释器或者编译器为了节省内存,而将具有符合某种规则的相同内容的对象放在同一块地址中。 Python 的 “驻留机制” 比较复杂,这里只说明其中关键的一点,即 判断字符串是否驻留的规则是:内容符合编程语言要求的命名规则,即由字母、数字或者下划线这三种字符所组成的字符串才会进行驻留。

这样看来,那4段程序中的 a b c d都是符合命名规则的,e f g h都是不符合命名规则的。那些诡异的实验现象也就得到了一个比较合理的解释。
到了这里,可能有人还记得,第4段程序在 Python3.8 下输出的可是 “True” 啊!这是赤裸裸地告诉我们,不同版本解释器之间的实现行为存在差异。
4. 一点建议
从关于这4段程序的输出结果,到关于 “is” 的实现原理和 “驻留机制” 的解释,我们似乎以为自己对这种诡异现象了如指掌。但是在两个不同版本解释器上运行结果的不一致再次对我们发布警告,也就是这里给 Python 使用者的一点建议。
- 不要对字符串的判断使用 “is”
- 忘记 “驻留机制”,永远也别打算利用这种机制
- 不要了解太多的Python底层,因为了解太多就可能在不知不觉中使用
日常使用,或许不会遇到那些诡异的“坑”,但多了解一些总是更有第底气一点。如果能将这几条建议变成习惯,那就不用考虑那么多了。
题外: 添加微信【cnbluegeek】备注“leetcode”或者“python”加入对应的群组

COMMENT
博客评论区功能由Github Issue提供,提交Issue时请以本文标题为话题。
"BG89-小心Python中的“is”"