【问题标题】:python strings are immutable. Why did this happen then?python 字符串是不可变的。为什么会发生这种情况?
【发布时间】:2022-06-10 12:43:17
【问题描述】:

Python 中的字符串是不可变的,这意味着值不能更改。我正在测试该场景,但看起来原始字符串已被修改。我只是想理解这个概念

>>> s = 'String'
>>> i = 5
>>> while i != 0:
...     s += str(i)
...     print(s + " stored at " + str(id(s)))
...     i -= 1
... 
String5 stored at 139841228476848
String54 stored at 139841228476848
String543 stored at 139841228476848
String5432 stored at 139841228476848
String54321 stored at 139841228476848
>>> a = "hello"
>>> id(a)
139841228475760
>>> a = "b" + a[1:]
>>> print(a)
bello
>>> id(a)
139841228475312

【问题讨论】:

  • @MohamadGhaithAlzin:docs,其中之一:“字符串是 Unicode 代码点的不可变序列。”
  • The standard wisdom is that Python strings are immutable. You can't change a string's value, only the reference to the string. continue reading here
  • @chouyangv3:你错了。 CPython 将字符串的核心数据存储在结构末尾的灵活数组成员中(它也可以将数据的其他副本存储在单独的数组中,但规范表示始终是内联分配的,与结构本身);如果字符串实际上被复制到一个新对象,id 会发生变化。 CPython 中的优化有时可以避免通过reallocing 进行复制,如果不能以其他方式检测到突变。
  • @chouyangv3:你需要了解 C 才能知道 CPython 参考解释器在这里做什么,特别是 flexible array members(在 C99 中标准化,但你可以在任何版本的 C 中模拟它们将长度为 1 的数组放在结构的末尾,并选择分配的不仅仅是sizeof(thestruct),或者只是分配额外的并将指向结构后字节的指针转换为正确的类型;旧的str 是前者, new str [with variable width characters] 后者)。
  • @user2357112 为什么说它破坏了不变性?我们所看到的是,之后的对象与之前的对象具有相同的地址。这并不意味着它们是同一个对象。

标签: python python-3.x string immutability string-concatenation


【解决方案1】:

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

你实际上不应该依赖它(非引用计数解释器不能使用这个技巧,因为他们不知道 str 是否有其他引用),每个 PEP8's very first programming recommendation

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

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

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

>>> while i!=0:
...     s += str(i)
...     s2 = s  # Gonna save off an alias here
...     print(s + " stored at " + str(id(s)))
...     i -= 1
... 

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

s = s + a + b

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

s += a + b

或:

s = s + (a + b)

通过确保最终连接始终是其中s 是左操作数并且结果用于立即替换s 来恢复优化。

【讨论】:

  • 我认为s2 = s 之前 += 会更清晰。
  • @KellyBundy:嗯,没关系。他们不打印原始字符串的id,所以他们永远不会看到它,而且碰巧的是,即使他们看到了,也不需要别名来为第一次传递获得新的id,因为原始的str 是一个完全由合法变量名字符组成的str 文字,它被保留(作为CPython 的实现细节),所以第一个连接总是会改变id。这是一个脆弱的优化,即使“次优”地破坏它仍然会彻底破坏它。
  • 我不是说因为第一次。我的意思是在动作之前获取第二个引用更清楚,而不是必须意识到 previous 迭代中的第二个引用是阻止 current 迭代中优化的原因.你和我都意识到,所以对我们来说这并没有太大的区别,但对于初学者来说,我想反过来会更清楚。
【解决方案2】:

不管实现细节如何,docs 说:

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

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

【讨论】:

  • 虽然根据the language data model spec,生命周期被定义为重叠,其中+= 被定义为x = x.__iadd__(y)x = x.__add__(y)x = y.__radd__(x) 之一(并且不可变类型是通过不实现来定义的)前者)。工作完成后延迟重新分配到x 是强制性的,正式的。在所有情况下,从方法返回的新对象必须在重新分配 x 之前存在,因此实际规范要求两者同时存在,因此 ids 必须有所不同,至少是短暂的。
  • 嗯...只是记得“至少短暂地”太宽容了。 ids 需要保持不同,因为所有对象的 id 都需要在其生命周期内保持不变,因此您不能执行恶作剧,例如将原始 strid 提供给新的 @ 987654337@.
猜你喜欢
  • 1970-01-01
  • 2021-01-24
  • 1970-01-01
  • 2013-11-06
  • 1970-01-01
  • 2021-04-30
  • 2010-09-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多