如果 Python 字符串是不可变的,为什么当我使用 += 附加到它时它会保持相同的 id?

Mas*_*shy 102 python string reference string-concatenation immutability

Python 中的字符串是不可变的,这意味着值不能更改。但是,当附加到以下示例中的字符串时,由于 id 保持不变,因此原始字符串内存看起来已被修改:

>>> s = 'String'
>>> for i in range(5, 0, -1):
...     s += str(i)
...     print(f"{s:<11} stored at {id(s)}")
... 
String5     stored at 139841228476848
String54    stored at 139841228476848
String543   stored at 139841228476848
String5432  stored at 139841228476848
String54321 stored at 139841228476848
Run Code Online (Sandbox Code Playgroud)

相反,在以下示例中,id 发生变化:

>>> a = "hello"
>>> id(a)
139841228475760
>>> a = "b" + a[1:]
>>> print(a)
bello
>>> id(a)
139841228475312
Run Code Online (Sandbox Code Playgroud)

Sha*_*ger 124

这是针对 CPython 特定的优化,适用于str附加到的对象恰好没有其他活动引用的情况。在这种情况下,解释器“作弊”,允许它通过重新分配(可以就地,取决于堆布局)和直接附加数据来修改现有字符串,并且通常会显着减少重复连接的循环中的工作(使其其行为更像是O(1)a 的摊销追加list,而不是O(n)每次的复制操作)。除了未更改之外,它没有任何可见的效果id,因此这样做是合法的(除非在逻辑上被替换str,否则任何现有对 a 的引用的人都不会看到它发生变化str)。

\n

你实际上不应该依赖它(非引用计数解释器不能使用这个技巧,因为它们不知道是否有str其他引用),根据PEP8 的第一个编程建议

\n
\n

代码的编写方式不应损害 Python 的其他实现(PyPy、Jython、IronPython、Cython、Psyco 等)。

\n
\n
\n

a += b例如,不要依赖 CPython\xe2\x80\x99s 对或形式的语句进行就地字符串连接的高效实现a = a + b。即使在 CPython 中,这种优化也是脆弱的(它仅适用于某些类型),并且在不使用引用计数的实现中根本不存在 xe2x80x99。在库的性能敏感部分,\'\'.join()应改用表单。这将确保在各种实现中串联在线性时间内发生。

\n
\n

如果你想打破优化,有多种方法可以做到这一点,例如将代码更改为:

\n
>>> while i!=0:\n...     s_alias = s  # Gonna save off an alias here\n...     s += str(i)\n...     print(s + " stored at " + str(id(s)))\n...     i -= 1\n... \n
Run Code Online (Sandbox Code Playgroud)\n

通过创建别名、增加引用计数并告诉 Python 更改将在 以外的其他地方可见s,因此它无法应用它,从而打破它。同样,代码如下:

\n
s = s + a + b\n
Run Code Online (Sandbox Code Playgroud)\n

无法使用它,因为s + a首先发生,并产生一个b必须添加到的临时值,而不是立即替换s,并且优化太脆弱,无法尝试处理该问题。几乎相同的代码,例如:

\n
s += a + b\n
Run Code Online (Sandbox Code Playgroud)\n

或者:

\n
s = s + (a + b)\n
Run Code Online (Sandbox Code Playgroud)\n

通过确保最终串联始终是左操作数之一来恢复优化s,并且结果用于立即替换s

\n

  • @KellyBundy:是的,这就是 PEP8 说不要依赖它的原因。它*易碎*。如果它能帮助您,那就太好了,但是如果您的代码“需要”它以获得足够的性能,则需要重写您的代码以使用 `list` + `''.join` 或 `io.StringIO` 或其他。(我已经删除了之前的评论,欢迎大家删除) (7认同)

Mar*_*nen 44

无论实现细节如何,文档都说:

\n
\n

\xe2\x80\xa6 具有不重叠生命周期的两个对象可能具有相同的 id() 值。

\n
\n

在之后 引用的前一个对象s不再存在,+=因此新对象具有相同的 不会违反规则id

\n

  • 不过,根据[语言数据模型规范](https://docs.python.org/3/reference/datamodel.html#object.__iadd__),生命周期被定义为重叠,其中“+=”被定义为以下之一`x = x.__iadd__(y)`、`x = x.__add__(y)` 或 `x = y.__radd__(x)` (不可变类型是通过不实现前者来定义的)。正式地,工作完成后延迟重新分配给“x”是强制性的。在所有情况下,从方法返回的新对象必须在重新分配“x”之前存在,因此实际规范要求两者同时存在,因此“id”必须不同,至少短暂不同。 (12认同)
  • @ShadowRanger 在这段短暂的重叠期间没有用户代码可以运行,因此无法检测该期间的共享 ID。这就是为什么这个优化是可以的。 (10认同)
  • @KellyBundy我是说,即使它们在概念上重叠,你也无法检测到它,因此优化器可以忽略该要求。 (7认同)
  • Python 字符串可变的一个更明确的例子是,当您计算字符串的哈希值时,结果会被缓存,这样解释器就不必再次计算同一字符串对象的哈希值。字符串哈希的缓存是该字符串结构的可变成员,它初始化为 -1,然后在计算哈希时被覆盖。所以这个结构无疑是可变的;只是无法观察到它发生变化(除非您将“time.perf_counter”视为观察它的一种方式)。 (2认同)