为什么字符串中有点时,"is"关键字的行为不同?

57 投票
2 回答
2710 浏览
提问于 2025-04-15 22:51

考虑一下这段代码:

>>> x = "google"
>>> x is "google"
True
>>> x = "google.com"
>>> x is "google.com"
False
>>>

为什么会这样呢?

为了确保上面的内容是正确的,我在Windows上测试了Python 2.5.4、2.6.5、2.7b2和Python 3.1,以及在Linux上测试了Python 2.7b1。

看起来这些版本之间是一致的,所以这是设计使然。我是不是漏掉了什么?

我发现我的一些个人域名过滤脚本因为这个问题而失败了。

2 个回答

15

"is" 是用来测试身份的。在 Python 中,对于小整数和(显然)字符串,有一些缓存的行为。通常来说,"is" 最适合用来检查单例对象,比如 None

>>> x = "google"
>>> x is "google"
True
>>> id(x)
32553984L
>>> id("google")
32553984L
>>> x = "google.com"
>>> x is "google.com"
False
>>> id(x)
32649320L
>>> id("google.com")
37787888L
92

is 用来检查对象的身份。在 Python 的任何实现中,当遇到不可变类型的字面量时,它可以选择 要么 创建一个新的不可变对象,要么 在现有的同类对象中查找,看看是否可以重用其中的某个对象(通过添加一个新的引用指向同一个底层对象)。这是一个实用的优化选择,并不受语义限制,因此你的代码不应该依赖于特定实现可能采取的路径(否则在 Python 的修复或优化更新中可能会出问题!)。

举个例子:

>>> import dis
>>> def f():
...   x = 'google.com'
...   return x is 'google.com'
... 
>>> dis.dis(f)
  2           0 LOAD_CONST               1 ('google.com')
              3 STORE_FAST               0 (x)

  3           6 LOAD_FAST                0 (x)
              9 LOAD_CONST               1 ('google.com')
             12 COMPARE_OP               8 (is)
             15 RETURN_VALUE    

所以在这个特定的实现中,在一个函数内部,你的观察不适用,只有一个对象会为字面量(任何字面量)创建,实际上:

>>> f()
True

从实用的角度来看,因为在函数内部通过本地常量表进行查找(为了节省内存,不创建多个常量不可变对象,而只创建一个)是相当便宜和快速的,并且可能会带来良好的性能回报,因为这个函数可能会被多次调用。

但是,同样的实现,在 交互提示符 下(编辑:我最初认为这也会发生在模块的顶层,但 @Thomas 的评论让我纠正了,见后文):

>>> x = 'google.com'
>>> y = 'google.com'
>>> id(x), id(y)
(4213000, 4290864)

并不会以这种方式尝试节省内存——id 是不同的,也就是说,它们是不同的对象。这样做可能会带来更高的成本和更低的回报,因此这个实现的优化器的启发式算法告诉它不必费心去查找,直接创建新的对象。

编辑:在模块顶层,根据 @Thomas 的观察,给定例如:

$ cat aaa.py
x = 'google.com'
y = 'google.com'
print id(x), id(y)

我们再次看到这个实现中的基于常量表的内存优化:

>>> import aaa
4291104 4291104

(根据 @Thomas 的观察结束编辑)。

最后,再谈谈同一个实现:

>>> x = 'google'
>>> y = 'google'
>>> id(x), id(y)
(2484672, 2484672)

这里的启发式算法不同,因为字面字符串“看起来可能是一个标识符”——所以它可能会在需要内存驻留的操作中被使用……因此优化器还是会将其驻留(而且一旦驻留,查找它会变得非常快)。确实,令人惊讶的是……:

>>> z = intern(x)
>>> id(z)
2484672

……x 在第一次时 已经被 intern(如你所见,intern 的返回值与 xy同一个 对象,因为它们的 id() 是相同的)。当然,你也不应该依赖于这一点——优化器并不 必须 自动驻留任何东西,这只是一个优化启发式;如果你需要 intern 的字符串,最好显式地进行驻留,以确保安全。当你 确实 显式驻留字符串时……:

>>> x = intern('google.com')
>>> y = intern('google.com')
>>> id(x), id(y)
(4213000, 4213000)

……那么你 确实 确保每次都得到完全相同的对象(即相同的 id())——这样你就可以应用微优化,比如用 is 而不是 == 来检查(我几乎从未发现这种微不足道的性能提升值得去费心;-)。

编辑:为了澄清,我在这里谈论的性能差异是,在一台慢速的 Macbook Air 上……:

$ python -mtimeit -s"a='google';b='google'" 'a==b'
10000000 loops, best of 3: 0.132 usec per loop
$ python -mtimeit -s"a='google';b='google'" 'a is b'
10000000 loops, best of 3: 0.107 usec per loop
$ python -mtimeit -s"a='goo.gle';b='goo.gle'" 'a==b'
10000000 loops, best of 3: 0.132 usec per loop
$ python -mtimeit -s"a='google';b='google'" 'a is b'
10000000 loops, best of 3: 0.106 usec per loop
$ python -mtimeit -s"a=intern('goo.gle');b=intern('goo.gle')" 'a is b'
10000000 loops, best of 3: 0.0966 usec per loop
$ python -mtimeit -s"a=intern('goo.gle');b=intern('goo.gle')" 'a == b'
10000000 loops, best of 3: 0.126 usec per loop

……最多也就几十纳秒的差别。所以,只有在极端的“优化这个性能瓶颈”的情况下,才值得 考虑 这些问题!-)

撰写回答