CPython并不保证默认情况下对所有字符串进行内部化,但实际上,Python代码库中的许多地方都会重用已创建的字符串对象。许多Python内部使用(C等效的)sys.intern()
函数调用来明确内部化Python字符串,但除非你遇到这些特殊情况之一,否则两个相同的Python字符串字面量将产生不同的字符串。
Python也可以自由地重用内存位置,并且Python还通过在编译时一次性存储不可变的文字来优化它们,并将其与代码对象中的字节码一起存储。Python REPL(交互式解释器)还将最近的表达式结果存储在_
名称中,这使得事情更加混乱。
因此,你会偶尔看到相同的id出现。
仅运行id(<string literal>)
行在REPL中经历了几个步骤:
The line is compiled, which includes creating a constant for the string object:
>>> compile("id('foo')", '<stdin>', 'single').co_consts
('foo', None)
This shows the stored constants with the compiled bytecode; in this case a string 'foo'
and the None
singleton. Simple expressions consisting of that produce an immutable value may be optimised at this stage, see the note on optimizers, below.
On execution, the string is loaded from the code constants, and id()
returns the memory location. The resulting int
value is bound to _
, as well as printed:
>>> import dis
>>> dis.dis(compile("id('foo')", '<stdin>', 'single'))
1 0 LOAD_NAME 0 (id)
3 LOAD_CONST 0 ('foo')
6 CALL_FUNCTION 1
9 PRINT_EXPR
10 LOAD_CONST 1 (None)
13 RETURN_VALUE
The code object is not referenced by anything, reference count drops to 0 and the code object is deleted. As a consequence, so is the string object.
Python可以重用相同的内存位置来创建新的字符串对象,如果您重新运行相同的代码。如果您多次重复此代码,则通常会导致打印相同的内存地址。这通常取决于您对Python内存的其他操作。
ID的重用是不可预测的;如果在此期间垃圾回收器运行以清除循环引用,则可能释放其他内存,并且您将获得新的内存地址。
接下来,Python编译器还将intern任何存储为常量的Python字符串,只要它看起来足够像有效的标识符即可。 Python
code object factory function PyCode_New将通过调用
intern_string_constants()
来intern包含仅ASCII字母,数字或下划线的任何字符串对象。该函数递归遍历常量结构,并针对找到的任何字符串对象
v
执行:
if (all_name_chars(v)) {
PyObject *w = v;
PyUnicode_InternInPlace(&v);
if (w != v) {
PyTuple_SET_ITEM(tuple, i, v);
modified = 1;
}
}
其中 all_name_chars()
被记录为
由于您创建了符合该标准的字符串,它们被放入池中,这就是为什么您在第二个测试中看到了相同 ID 被用于 'so' 字符串的原因:只要对池中版本的引用存在,池化就会导致未来的 'so' 文本重用池中的字符串对象,即使在新的代码块中并绑定到不同的标识符。在您的第一个测试中,您没有保存对字符串的引用,因此池化的字符串在被重复使用之前被丢弃。
顺便说一下,您的新名称 so = 'so' 将字符串绑定到一个包含相同字符的名称中。换句话说,您正在创建一个名称和值相等的全局变量。由于 Python 会对标识符和限定常量进行池化,因此最终使用相同的字符串对象作为标识符及其值。
>>> compile("so = 'so'", '<stdin>', 'single').co_names[0] is compile("so = 'so'", '<stdin>', 'single').co_consts[0]
True
如果您创建的字符串既不是代码对象常量,也包含字母+数字+下划线范围之外的字符,则会看到
id()
值不被重用:
>>> some_var = 'Look ma, spaces and punctuation!'
>>> some_other_var = 'Look ma, spaces and punctuation!'
>>> id(some_var)
4493058384
>>> id(some_other_var)
4493058456
>>> foo = 'Concatenating_' + 'also_helps_if_long_enough'
>>> bar = 'Concatenating_' + 'also_helps_if_long_enough'
>>> foo is bar
False
>>> foo == bar
True
Python编译器使用
peephole optimizer(Python版本<3.7)或更强大的
AST optimizer(3.7及更新版本)来预先计算(折叠)涉及常量的简单表达式的结果。Peepholder将其输出限制为20个或更少的序列(以防止代码对象和内存使用膨胀),而AST优化器对于长度为4096个字符的字符串使用单独的限制。这意味着,如果连接仅由名称字符组成的较短字符串的结果字符串符合当前Python版本的优化器限制,则仍然可以导致内部化字符串。
例如,在Python 3.7上,
'foo'*20
将导致单个内部化字符串,因为常量折叠将其转换为单个值,而在Python 3.6或更早版本中,只有
'foo'*6
会被折叠:
>>> import dis, sys
>>> sys.version_info
sys.version_info(major=3, minor=7, micro=4, releaselevel='final', serial=0)
>>> dis.dis("'foo' * 20")
1 0 LOAD_CONST 0 ('foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo')
2 RETURN_VALUE
并且
>>> dis.dis("'foo' * 6")
1 0 LOAD_CONST 2 ('foofoofoofoofoofoo')
2 RETURN_VALUE
>>> dis.dis("'foo' * 7")
1 0 LOAD_CONST 0 ('foo')
2 LOAD_CONST 1 (7)
4 BINARY_MULTIPLY
6 RETURN_VALUE