Python字符串内部化

120

虽然这个问题在实践中并没有实际用途,但我很好奇 Python 如何执行字符串内联。我已经注意到以下内容。

>>> "string" is "string"
True

这正如我所预料的那样。

你也可以做到这一点。

>>> "strin"+"g" is "string"
True

这相当聪明!

但你不能这样做。

>>> s1 = "strin"
>>> s2 = "string"
>>> s1+"g" is s2
False
为什么Python不会计算`s1+"g"`,然后意识到它与`s2`相同并将其指向相同的地址? 实际上,在最后一个块中发生了什么来使它返回`False`?
2个回答

121
这是实现特定的行为,但您的解释器可能会在编译时对常量进行内部化处理,但不会对运行时表达式的结果进行该处理。
以下内容使用的是CPython 3.9.0+。
在第二个示例中,表达式“string” + “g”在编译时被评估,并被替换为“string”。这使得前两个示例的行为相同。
如果我们检查字节码,我们将看到它们完全相同。
  # s1 = "string"
  1           0 LOAD_CONST               0 ('string')
              2 STORE_NAME               0 (s1)

  # s2 = "strin" + "g"
  2           4 LOAD_CONST               0 ('string')
              6 STORE_NAME               1 (s2)

这个字节码是由(它会在上述内容后打印几行)生成的:

import dis

source = 's1 = "string"\ns2 = "strin" + "g"'
code = compile(source, '', 'exec')
print(dis.dis(code))
第三个示例涉及运行时的字符串连接操作,其结果不自动进行字符串常量池优化。
  # s3a = "strin"
  3           8 LOAD_CONST               1 ('strin')
             10 STORE_NAME               2 (s3a)

  # s3 = s3a + "g"
  4          12 LOAD_NAME                2 (s3a)
             14 LOAD_CONST               2 ('g')
             16 BINARY_ADD
             18 STORE_NAME               3 (s3)
             20 LOAD_CONST               3 (None)
             22 RETURN_VALUE

这个字节码是通过(它会在上面打印几行,并且那些行与上面给出的第一个字节码块完全相同)获得的:

import dis

source = (
    's1 = "string"\n'
    's2 = "strin" + "g"\n'
    's3a = "strin"\n'
    's3 = s3a + "g"')
code = compile(source, '', 'exec')
print(dis.dis(code))

如果您手动使用 sys.intern() 内置函数处理第三个表达式的结果,您将获得与之前相同的对象:

>>> import sys
>>> s3a = "strin"
>>> s3 = s3a + "g"
>>> s3 is "string"
False
>>> sys.intern(s3) is "string"
True

此外,Python 3.9 对于上述最后两个语句会打印一个警告:

SyntaxWarning: "is" with a literal. Did you mean "=="?


33
记录一下:Python的窥孔优化会在编译时预先计算常量的算术运算("string1" + "s2", 10 + 3*20, 等等),但将生成的序列限制在20个元素以内(防止[None] * 10**1000过度扩展字节码)。正是这种优化把"strin" + "g" 转换成了 "string",结果长度小于20个字符。 - Martijn Pieters
17
为了更加清晰明确:这里并没有进行任何内部化。不可变字面量被作为常量存储在字节码中。代码中使用的名称会进行内部化,但程序创建的字符串值除非通过intern()函数特别进行内部化,否则不会进行内部化。 - Martijn Pieters
11
对于那些试图在Python 3中查找intern函数的人 - 它已经移动到sys.intern - Timofey Chernousov

5

案例1

>>> x = "123"  
>>> y = "123"  
>>> x == y  
True  
>>> x is y  
True  
>>> id(x)  
50986112  
>>> id(y)  
50986112  

案例2

>>> x = "12"
>>> y = "123"
>>> x = x + "3"
>>> x is y
False
>>> x == y
True

现在,您的问题是为什么在情况1中id相同而在情况2中不同。
在情况1中,您已将字符串常量“123”分配给x和y。
由于字符串是不可变的,解释器只需要存储一次字符串文字并将所有变量指向同一个对象,这是有意义的。
因此您会看到id是相同的。
在情况2中,您正在使用连接修改x。Both x和y具有相同的值,但不具有相同的身份。
它们都指向内存中的不同对象。因此它们具有不同的id和is运算符返回False。

为什么字符串是不可变的,将x +“3”赋值(并寻找新的存储位置)不会分配给与y相同的引用? - nicecatch
因为这样它需要将新字符串与所有现有字符串进行比较;这可能是一个非常昂贵的操作。我想它可以在赋值后在后台执行此操作,以减少内存使用,但这样你最终会得到更奇怪的行为:例如 id(x) != id(x),因为在评估过程中字符串被移动了。 - DylanYoung
1
@AndreaConte,因为字符串的连接不会额外地查找所有已使用字符串的池子,每次生成新字符串。另一方面,解释器将表达式x =“12”+“3”“优化”为x =“123”(在单个表达式中连接两个字符串字面量),因此赋值实际上进行查找,并找到与y =“123”相同的“内部”字符串。 - derenio
实际上,不是赋值执行查找,而是源代码中的每个字符串字面量都被“内部化”,并且该对象在所有其他位置重复使用。 - derenio

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