<p>默认情况下,CPython并不保证将所有字符串都实习,但实际上,Python代码库中的许多地方确实重用了已经创建的字符串对象。很多Python内部都使用<a href="https://docs.python.org/3/library/sys.html#sys.intern" rel="noreferrer">^{<cd1>} function call</a>来显式地内联Python字符串,但是除非遇到其中一种特殊情况,否则两个相同的Python字符串文本将生成不同的字符串。</p>
<p>Python还可以自由地重用内存位置,并且Python还可以通过在编译时使用代码对象中的字节码存储不可变的文本来优化它们。Python REPL(交互式解释器)还将最新的表达式结果存储在<code>_</code>名称中,这会使事情更加混乱。</p>
<p>因此,您将不时看到相同的id出现。</p>
<p>只运行REPL中的<code>id(<string literal>)</code>行需要几个步骤:</p>
<ol>
<li><p>该行已编译,其中包括为字符串对象创建常量:</p>
<pre><code>>>> compile("id('foo')", '<stdin>', 'single').co_consts
('foo', None)
</code></pre>
<p>这显示了已编译字节码的存储常量;在本例中是字符串<code>'foo'</code>和<code>None</code>单例。在此阶段,可以优化由产生不可变值的简单表达式组成的表达式,请参见下面关于优化器的说明。</p></li>
<li><p>执行时,从代码常量加载字符串,并<code>id()</code>返回内存位置。生成的<code>int</code>值绑定到<code>_</code>,并打印:</p>
<pre><code>>>> 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
</code></pre></li>
<li><p>代码对象未被任何对象引用,引用计数降至0,代码对象将被删除。因此,string对象也是如此。</p></li>
</ol>
<p>然后,如果重新运行相同的代码,Python可以将相同的内存位置用于新的string对象。如果重复此代码,通常会导致打印相同的内存地址。<em>这取决于您对Python内存</em>还做了什么。</p>
<p>ID重用不是可预测的;如果同时垃圾收集器运行以清除循环引用,则可以释放其他内存,您将获得新的内存地址。</p>
<p>接下来,Python编译器还将实习生存储为常量的任何Python字符串,只要它看起来足够像一个有效的标识符。Python<a href="https://github.com/python/cpython/blob/a6eac83c1804fd14ed076b1776ffeea8dcb9478a/Objects/codeobject.c#L96-L222" rel="noreferrer">code object factory function PyCode_New</a>将通过调用<a href="https://github.com/python/cpython/blob/a6eac83c1804fd14ed076b1776ffeea8dcb9478a/Objects/codeobject.c#L45-L93" rel="noreferrer">^{<cd9>}</a>来实习生任何只包含ASCII字母、数字或下划线的字符串对象。此函数在常量结构中递归,对于在其中找到的任何字符串对象<code>v</code>执行:</p>
<pre class="lang-c prettyprint-override"><code>if (all_name_chars(v)) {
PyObject *w = v;
PyUnicode_InternInPlace(&v);
if (w != v) {
PyTuple_SET_ITEM(tuple, i, v);
modified = 1;
}
}
</code></pre>
<p>其中<a href="https://github.com/python/cpython/blob/a6eac83c1804fd14ed076b1776ffeea8dcb9478a/Objects/codeobject.c#L13-L29" rel="noreferrer">^{<cd11>}</a>被记录为</p>
<pre class="lang-c prettyprint-override"><code>/* all_name_chars(s): true iff s matches [a-zA-Z0-9_]* */
</code></pre>
<p>因为您创建了符合该条件的字符串,所以它们被保留,这就是为什么您在第二个测试中看到对<code>'so'</code>字符串使用相同的ID的原因:只要保留对保留版本的引用,保留将导致未来的<code>'so'</code>文本重用被保留的字符串对象,即使是在新的代码块和绑定到不同的标识符。在第一个测试中,不保存对字符串的引用,因此在可以重用之前,将丢弃已实习的字符串。</p>
<p>顺便说一下,您的新名称<code>so = 'so'</code>将字符串绑定到包含相同字符</em>的名称。换句话说,您正在创建一个名称和值相等的全局。作为Python的标识符和限定常量的实习生,您最终将对标识符及其值使用相同的字符串对象:</p>
<pre><code>>>> compile("so = 'so'", '<stdin>', 'single').co_names[0] is compile("so = 'so'", '<stdin>', 'single').co_consts[0]
True
</code></pre>
<p>如果您创建的字符串不是代码对象常量,或者包含字母+数字+下划线范围以外的字符,您将看到<code>id()</code>值未被重用:</p>
<pre><code>>>> 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
</code></pre>
<p>Python编译器要么使用<a href="https://github.com/python/cpython/blob/3.6/Python/peephole.c" rel="noreferrer">peephole optimizer</a>(Python版本<;3.7)要么使用功能更强大的<a href="https://github.com/python/cpython/blob/3.7/Python/ast_opt.c" rel="noreferrer">AST optimizer</a>(3.7及更新版本)来预计算(折叠)包含常量的简单表达式的结果。peepholder将其输出限制为长度小于等于20的序列(以防止代码对象膨胀和内存使用),而AST优化器对4096个字符的字符串使用单独的限制呃。这意味着,如果生成的字符串符合当前Python版本的优化器限制,则仅由名称字符<em>组成的较短字符串的连接仍然会导致字符串被中断。</p>
<p>例如,在Python 3.7上,<code>'foo' * 20</code>将产生一个单独的内部字符串,因为常量折叠将其转换为单个值,而在python3.6或更早版本上,只有<code>'foo' * 6</code>将被折叠:</p>
<pre><code>>>> 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
</code></pre>
<p>以及</p>
<pre><code>>>> 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
</code></pre>