在Python中操作字符串时,有时可能会遇到一些奇怪的现象,例如下面这个例子:
>>> a = "hello">>> b = "hello">>> a is bTrue>>> a = "hello world">>> b = "hello world">>> a is bFalse复制代码
你可能会问:为什么会这样呢?答案是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 == bTrue复制代码
由此可以知道,如果a is b
为True
,也就是说 a 和 b 指向同一个内存地址,那么a == b
也必定为True
。这点没有任何疑惑。
但是回到文章开头处,有:
>>> a = "hello world">>> b = "hello world">>> a is bFalse复制代码
上面这个结果向我们透露了两个信息:
- a 和 b 指向不同的内存地址
- 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 bTrue>>> a = "kjhuwhoehiwh98yu398y1____ajs9f9">>> b = "kjhuwhoehiwh98yu398y1____ajs9f9">>> a is bTrue>>> a = "python is great!">>> b = "python is great!">>> a is bFalse复制代码
编译时常量和运行时表达式
下面介绍一个更加让人迷惑的现象:
>>> 'a' + 'b' is 'ab'True>>> a = 'a'>>> a + 'b' is 'ab'False复制代码
我们用dis
包将上面的代码编译成Python字节码:
import disdef 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))复制代码
运行结果:
TrueFalse************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_VALUENone************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_VALUENone复制代码
大家可以看到,如果常量的值能够在编译时就确定,那么就会被驻留;如果必须在运行时才能确定,那么就不会驻留。你还可以尝试下面的例子:
>>> a = 'a'>>> a * 20 is 'aaaaaaaaaaaaaaaaaaaa' # a * 20 在编译时无法确定其值False>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' # 'a' * 20 在编译时可以确定其值True复制代码
另外还可以尝试:
>>> x, y = 'hello', 'hello'>>> x is yTrue>>> x, y = 'hello!', 'hello!'>>> x is yFalse复制代码
总结
两点:
- 当且仅当只包含下划线、数字、字母的字符串才会被驻留;
- 编译时可以被确定的常量值会被驻留,运行时才能确定的值不会指向编译时驻留的字符串。
以上结果,均在Python 3.7.2中验证。更低版本的Python可能会出现不同的结果。如果出现了自己不能理解的结果,建议使用dis
包,将代码编译为字节码,即可明白其原理。
Python交流学习群:
微信公众号:小鑫的代码日常