Python札记1:字符串驻留(String Interning)

在Python中操作字符串时,有时可能会遇到一些奇怪的现象,例如下面这个例子:

>>> a = "hello"
>>> b = "hello"
>>> a is b
True
>>> a = "hello world"
>>> b = "hello world"
>>> a is b
False

你可能会问:为什么会这样呢?答案是Python中有一种称为“字符串驻留(String Internning)”的机制。

is和==

在Python中,我们使用is来判断两个对象的对象标识符(object identity)是否相等,也就是判断两个对象的内存地址是否相等,是不是同一个东西,即a is b相当于检查id(a) == id(b)

>>> a = "hello"
>>> b = "hello"
>>> id(a) == id(b)
True

==只是用于判断两个对象的值是否相等(equality),也就是说,两个变量的值是否相等:

>>> a = "hello"
>>> b = "hello"
>>> a == b
True

由此可以知道,如果a is bTrue,也就是说 a 和 b 指向同一个内存地址,那么a == b也必定为True。这点没有任何疑惑。

但是回到文章开头处,有:

>>> a = "hello world"
>>> b = "hello world"
>>> a is b
False

上面这个结果向我们透露了两个信息:

  1. a 和 b 指向不同的内存地址
  2. a 和 b 的值是相同的

我们可以用id函数来查看对象的标识符(地址):

>>> b = "hello world"
>>> a = "hello world"
>>> id(a)
1580069459248
>>> id(b)
1580069232944

可以看到,a 和 b 确实是不同的对象。产生这种情况的原因就是字符串驻留(String Interning)。

字符串驻留(String Interning)

Python中的字符串采用了驻留机制,当需要值相同的字符串的时候(比如标识符),可以直接从字符串池里拿来使用,也就是值相同的字符串在内存中只有一个对象。

这样做是为了避免频繁的创建和销毁,提升效率和节约内存。

因此拼接和修改字符串是会比较影响性能的。因为Python中的字符串是不可变的,所以字符串的操作都不是原址操作,而是新建对象,这也是为什么拼接多字符串的时候不建议用加号(+),而用join(),join()是先计算出所有字符串的长度,然后再拷贝,只new一次对象。

需要注意的是,并不是所有的字符串都会采用驻留机制,当且仅当只包含下划线、数字、字母的字符串才会被驻留。

驻留字符串和不可驻留字符串

因为 “hello world” 中包含了空格,所以不会驻留。如果满足驻留要求,那么就会驻留:

>>> a = "helloworld"
>>> b = "helloworld"
>>> a is b
True
>>> a = "kjhuwhoehiwh98yu398y1____ajs9f9"
>>> b = "kjhuwhoehiwh98yu398y1____ajs9f9"
>>> a is b
True
>>> a = "python is great!"
>>> b = "python is great!"
>>> a is b
False

编译时常量和运行时表达式

下面介绍一个更加让人迷惑的现象:

>>> 'a' + 'b' is 'ab'
True
>>> a = 'a'
>>> a + 'b' is 'ab'
False

我们用dis包将上面的代码编译成Python字节码:

import dis

def bytecode1():
    a = 'a' + 'b'
    print(a is 'ab')
    
def bytecode2():
    a = 'a'
    b = a + 'b'
    print(b is 'ab')

if __name__ == "__main__":
    bytecode1()
    bytecode2()
    print("************compile-time*************")
    print(dis.dis(bytecode1))
    print("************run-time*************")
    print(dis.dis(bytecode2))

运行结果:

True
False
************compile-time*************
  4           0 LOAD_CONST               1 ('ab')
              2 STORE_FAST               0 (a)

  5           4 LOAD_GLOBAL              0 (print)
              6 LOAD_FAST                0 (a)
              8 LOAD_CONST               1 ('ab')
             10 COMPARE_OP               8 (is)
             12 CALL_FUNCTION            1
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE
None
************run-time*************
  8           0 LOAD_CONST               1 ('a')
              2 STORE_FAST               0 (a)

  9           4 LOAD_FAST                0 (a)
              6 LOAD_CONST               2 ('b')
              8 BINARY_ADD
             10 STORE_FAST               1 (b)

 10          12 LOAD_GLOBAL              0 (print)
             14 LOAD_FAST                1 (b)
             16 LOAD_CONST               3 ('ab')
             18 COMPARE_OP               8 (is)
             20 CALL_FUNCTION            1
             22 POP_TOP
             24 LOAD_CONST               0 (None)
             26 RETURN_VALUE
None

大家可以看到,如果常量的值能够在编译时就确定,那么就会被驻留;如果必须在运行时才能确定,那么就不会驻留。你还可以尝试下面的例子:

>>> a = 'a'
>>> a * 20 is 'aaaaaaaaaaaaaaaaaaaa' # a * 20 在编译时无法确定其值
False
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' # 'a' * 20 在编译时可以确定其值
True

另外还可以尝试:

>>> x, y = 'hello', 'hello'
>>> x is y
True
>>> x, y = 'hello!', 'hello!'
>>> x is y
False

总结

两点:

  • 当且仅当只包含下划线、数字、字母的字符串才会被驻留;
  • 编译时可以被确定的常量值会被驻留,运行时才能确定的值不会指向编译时驻留的字符串。

以上结果,均在Python 3.7.2中验证。更低版本的Python可能会出现不同的结果。如果出现了自己不能理解的结果,建议使用dis包,将代码编译为字节码,即可明白其原理。


我的知乎:奔三的鑫鑫

欢迎关注微信公众号:小鑫的代码日常

欢迎加入Python学习交流群:532232743,这里有各路高手等着你~

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页