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

57

考虑以下代码:

>>> 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。
看起来它们之间是一致的,所以这是有意设计的。我错过了什么吗?
我刚刚发现我的一些个人域名过滤脚本也遇到了这个问题。

10
好抓到。那个有点奇怪。 - Adam Crossland
2
在Python 2.5.2中,两者都是False - Håvard S
3
在ActivePython 2.5.4.4中,我看到与OP相同的结果。几乎肯定与字符串驻留有关,对吗? - Adam Crossland
3
谢谢@ire,看来我误用了is运算符! - YOU
2个回答

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任何内容,它只是一个优化启发式算法; 如果你需要intern字符串,最好明确指定。当你显式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

最多误差在几十纳秒内,所以只有在极端的“优化这个[删除的脏话]性能瓶颈”情况下才值得考虑!-)


9
似乎我误用了 is 运算符。 - YOU
1
@S.Mark,可能,但不一定——请看我的关于interning的编辑。通常情况下,您应该仅在可变对象(如列表)和单例对象(如None)上使用is,但是如果您已经确保了interning(作为微小的优化),那么您也可以在这里使用它(即使您真的虔诚地intern所有相关字符串,interning也会使==检查稍微快一点!)。 - Alex Martelli
2
Alex,你说“在模块的顶层(或交互提示符)”,但我认为你描述的(以及OP看到的)只会发生在交互提示符--模块的顶层仍然编译成单个代码对象,并且该代码对象中所有对相同常量的引用使用相同的引用。 - Thomas Wouters
@Thomas,你说得对——我已经很久没有仔细看过那些具体的内部细节了。谢谢!让我编辑一下来修复... - Alex Martelli
交互提示符中关于字符串看起来像标识符的行为只与字面字符串有关。在表达式创建的字符串中会出现一些非常奇怪的行为。例如,在Python 2.6和3.2中: >>> 'a'*20 is 'a'*20 True >>> 'a'*21 is 'a'*21 False - Mitchell Model

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

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