困惑为什么在不可变字符串的 += 运算符的第二次评估后不会更改 Python3 中的 id

Confused why after 2nd evaluation of += operator of immutable string does not change the id in Python3

我正在使用 Python 3.8.3 & 我在检查字符串的 id 时得到了如下所示的意外输出。

>>> a="d"
>>> id(a)
1984988052656
>>> a+="e"
>>> id(a)
1985027888368
>>> a+="h"
>>> id(a)
1985027888368
>>> a+="i"
>>> id(a)
1985027888368
>>> 

在a后面加上"h"之后,id(a)没有变化。当字符串不可变时,这怎么可能? 当我在 .py 文件中使用 a=a+"h" 而不是 a+="h" 和 运行 时,我得到了相同的输出(我提到因为在某些情况下我们可以看到不同的输出运行ning 在 shell 和 运行ning 保存到文件后的相同代码)

这里有些猜测 - 当 GC 运行时,允许 compact/reorganize 内存。通过这样做,它完全有权重用旧地址,只要它们现在是免费的。通过调用 a+="h",您创建了一个新的不可变字符串,但丢失了对先前指向的字符串 a 的引用。此字符串符合垃圾回收条件,这意味着它曾经占用的旧地址可以重新使用。

这只有在字节码评估循环中对字符串连接进行了奇怪的、略微粗略的优化后才有可能。 INPLACE_ADD 实现特例两个字符串对象:

case TARGET(INPLACE_ADD): {
    PyObject *right = POP();
    PyObject *left = TOP();
    PyObject *sum;
    if (PyUnicode_CheckExact(left) && PyUnicode_CheckExact(right)) {
        sum = unicode_concatenate(tstate, left, right, f, next_instr);
        /* unicode_concatenate consumed the ref to left */
    }
    else {
        ...

并调用 unicode_concatenate helper that delegates to PyUnicode_Append,它试图就地改变原始字符串:

void
PyUnicode_Append(PyObject **p_left, PyObject *right)
{
    ...
    if (unicode_modifiable(left)
        && PyUnicode_CheckExact(right)
        && PyUnicode_KIND(right) <= PyUnicode_KIND(left)
        /* Don't resize for ascii += latin1. Convert ascii to latin1 requires
           to change the structure size, but characters are stored just after
           the structure, and so it requires to move all characters which is
           not so different than duplicating the string. */
        && !(PyUnicode_IS_ASCII(left) && !PyUnicode_IS_ASCII(right)))
    {
        /* append inplace */
        if (unicode_resize(p_left, new_len) != 0)
            goto error;

        /* copy 'right' into the newly allocated area of 'left' */
        _PyUnicode_FastCopyCharacters(*p_left, left_len, right, 0, right_len);
    }
    ...

只有在 unicode_concatenate 可以保证没有其他对 LHS 的引用时才会进行优化。您最初的 a="d" 有其他引用,因为 Python 使用 Latin-1 范围内的 1 个字符的字符串缓存,因此优化没有触发。在其他一些情况下,优化也可能无法触发,例如 LHS 具有缓存的哈希,或者 realloc 需要移动字符串(在这种情况下,大部分优化的代码路径都会执行,但不会' 就地执行操作成功)。


此优化违反了 id+= 的正常规则。通常,不可变对象上的 += 应该在清除对旧对象的引用之前创建一个新对象,因此新对象和旧对象应该具有重叠的生命周期,禁止相等的 id 值。优化到位后,+= 之后的字符串与 +=.

之前的字符串具有相同的 ID

语言开发人员决定他们更关心那些将字符串连接放在循环中、看到糟糕的性能并认为 Python 糟糕的人,而不是他们关心这个晦涩的技术点。