关于更改不可变字符串的id

Bac*_*ach 43 python string immutability python-internals

关于id类型对象的某些东西str(在python 2.7中)让我很困惑.该str类型是不变的,所以我希望,一旦它被创建,它将始终具有相同的id.我相信我不会这么说自己,所以我会发布一个输入和输出序列的例子.

>>> id('so')
140614155123888
>>> id('so')
140614155123848
>>> id('so')
140614155123808
Run Code Online (Sandbox Code Playgroud)

所以同时,它一直在变化.但是,在指向该字符串的变量之后,事情会发生变化:

>>> so = 'so'
>>> id('so')
140614155123728
>>> so = 'so'
>>> id(so)
140614155123728
>>> not_so = 'so'
>>> id(not_so)
140614155123728
Run Code Online (Sandbox Code Playgroud)

因此,一旦变量保存该值,它就会冻结id.的确,在del so和之后del not_so,id('so')开始的输出再次改变.

这是相同的行为与(小)整数.

我知道不变性和拥有相同之间没有真正的联系id; 仍然,我试图弄清楚这种行为的来源.我相信那些熟悉python内部的人会比我更少惊讶,所以我试图达到同样的目的......

更新

尝试使用不同的字符串会产生不同的结果......

>>> id('hello')
139978087896384
>>> id('hello')
139978087896384
>>> id('hello')
139978087896384
Run Code Online (Sandbox Code Playgroud)

现在它平等的......

Mar*_*ers 66

CPython 默认承诺实习字符串,但实际上,Python代码库中的很多地方都会重用已经创建的字符串对象.许多Python内部使用(C等效)sys.intern()函数调用显式实习Python字符串,但除非你遇到其中一个特殊情况,两个相同的Python字符串文字将产生不同的字符串.

Python也可以自由地重用内存位置,Python也可以通过在编译时将代码对象中的字节码存储一次来优化不可变文本.Python REPL(交互式解释器)还将最新的表达式结果存储在_名称中,这使得事情更加混乱.

因此,您看到同时出现相同的ID.

id(<string literal>)在REPL中仅运行该行需要执行以下几个步骤:

  1. 该行被编译,包括为字符串对象创建一个常量:

    >>> compile("id('foo')", '<stdin>', 'single').co_consts
    ('foo', None)
    
    Run Code Online (Sandbox Code Playgroud)

    这显示了存储的常量和编译的字节码; 在这种情况下是一个字符串'foo'None单例.

  2. 执行时,从代码常量加载字符串,并id()返回内存位置.结果int值必然会_被打印:

    >>> 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        
    
    Run Code Online (Sandbox Code Playgroud)
  3. 任何东西都不引用代码对象,引用计数降为0并删除代码对象.因此,字符串对象也是如此.

如果重新运行相同的代码,Python可能为新的字符串对象重用相同的内存位置.如果重复此代码,这通常会导致打印相同的内存地址.这取决于你对Python内存做了什么.

ID重用是不可预测的; 如果在此期间垃圾收集器运行以清除循环引用,则可以释放其他内存并且您将获得新的内存地址.

接下来,Python编译器还将实现任何存储为常量的Python字符串,前提是它看起来像一个有效的标识符.Python 代码对象工厂函数PyCode_New将实际包含只包含ASCII字母,数字或下划线的任何字符串对象:

if (all_name_chars(v)) {
    PyObject *w = v;
    PyUnicode_InternInPlace(&v);
    if (w != v) {
        PyTuple_SET_ITEM(tuple, i, v);
        modified = 1;
    }
}
Run Code Online (Sandbox Code Playgroud)

既然你创建了符合标准的字符串,它们被拘留,这就是为什么你看到正在使用相同的ID为intern_string_constants()字符串在你的第二个测试:只要给实习版本的引用生存,实习会导致未来v文字重用interned string对象,即使在新的代码块中也绑定到不同的标识符.在第一次测试中,您不保存对字符串的引用,因此在重用之前会丢弃实例化的字符串.

顺便提一下,您的新名称all_name_chars()将字符串绑定到包含相同字符的名称.换句话说,您正在创建一个名称和值相等的全局.由于Python实例化了标识符和限定常量,因此最终对标识符及其值使用相同的字符串对象:

/* all_name_chars(s): true iff s matches [a-zA-Z0-9_]* */
Run Code Online (Sandbox Code Playgroud)

如果您创建的字符串不是代码对象常量,或者包含字母+数字+下划线范围之外的字符,您将看到'so'不重用的值:

>>> compile("so = 'so'", '<stdin>', 'single').co_names[0] is compile("so = 'so'", '<stdin>', 'single').co_consts[0]
True
Run Code Online (Sandbox Code Playgroud)

Python窥孔优化器会预先计算简单表达式的结果,但如果这导致序列长于20,则忽略输出(以防止膨胀的代码对象和内存使用); 如果结果是20个字符或更短,那么连接只包含名称字符的较短字符串仍然可以导致实习字符串.

  • @MariusMucenicu 是的,我的回答概括地描述了该算法。您还可以在 Python 源代码中 grep 调用 [`PyUnicode_InternInPlace` 和 `PyUnicode_InternFromString` 函数](https://docs.python.org/3/c-api/unicode.html#c.PyUnicode_InternInPlace) 来查看其中的位置Python 是实习字符串(例如在 GitHub 上搜索 [either](https://github.com/python/cpython/search?l=C&amp;q=PyUnicode_InternInPlace) [function](https://github.com/python/cpython/搜索?l=C&amp;q=PyUnicode_InternFromString)).. (2认同)