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



关于str类型对象的id(在python 2.7中)的一些问题使我感到困惑。str类型是不可变的,所以我希望一旦它被创建,它将始终具有相同的id。我相信我没有很好地表达自己,所以我将发布一个输入和输出序列的例子。

>>> id('so')
140614155123888
>>> id('so')
140614155123848
>>> id('so')
140614155123808

同时,它一直在变化。但是,在有了指向该字符串的变量之后,情况就改变了:

>>> so = 'so'
>>> id('so')
140614155123728
>>> so = 'so'
>>> id(so)
140614155123728
>>> not_so = 'so'
>>> id(not_so)
140614155123728

看起来它冻结了id,一旦一个变量持有该值。事实上,在del sodel not_so之后,id('so')的输出又开始发生变化。

这是而不是与(小)整数相同的行为。

我知道在不变性和拥有相同的id之间没有真正的联系;尽管如此,我仍在试图找出这种行为的根源。我相信那些熟悉python内部结构的人不会比我更惊讶,所以我试图达到同样的观点…

<标题> 更新

对不同的字符串进行相同的尝试会得到不同的结果…

>>> id('hello')
139978087896384
>>> id('hello')
139978087896384
>>> id('hello')
139978087896384

现在它等于…

CPython不承诺在默认情况下调用所有字符串,但在实践中,Python代码库中的许多地方确实重用了已经创建的字符串对象。许多Python内部使用(相当于c语言的)sys.intern()函数调用来显式地调用Python字符串,但除非遇到其中一种特殊情况,否则两个相同的Python字符串字面量将产生不同的字符串。

Python还可以自由地重用内存位置,并且Python还将优化不可变字面值,在编译时将它们与代码对象中的字节码一起存储一次。Python REPL(交互式解释器)还将最新的表达式结果存储在_名称中,这会使事情更加混乱。

因此,您看到相同的id不时出现。

在REPL中只运行id(<string literal>)行需要经过几个步骤:

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

    >>> compile("id('foo')", '<stdin>', 'single').co_consts
    ('foo', None)
    

    显示已编译字节码中存储的常量;在本例中是字符串'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        
    
  3. 代码对象不被任何对象引用,引用计数降为0,代码对象被删除。因此,string对象也是如此。

如果您重新运行相同的代码,那么Python可以也许为新的字符串对象重用相同的内存位置。如果重复此代码,通常会导致打印相同的内存地址。这取决于你如何使用Python内存

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

接下来,Python编译器也会将任何作为常量存储的Python字符串导入,只要它看起来足够像一个有效的标识符。Python代码对象工厂函数PyCode_New将通过调用intern_string_constants()调用任何只包含ASCII字母、数字或下划线的字符串对象。这个函数通过常量结构递归,对于v找到的任何字符串对象执行:

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

其中all_name_chars()文档为

/* all_name_chars(s): true iff s matches [a-zA-Z0-9_]* */

因为您创建了符合该标准的字符串,所以它们是被拘禁的,这就是为什么您在第二个测试中看到相同的ID被用于'so'字符串:只要对被拘禁版本的引用存在,拘禁将导致未来的'so'字面值重用被拘禁的字符串对象,即使在新的代码块中并且绑定到不同的标识符。在您的第一个测试中,您没有保存对字符串的引用,因此在可以重用之前,内部字符串被丢弃。

顺便提一下,您的新名称so = 'so'将字符串绑定到包含相同字符的名称。换句话说,您正在创建一个名称和值相等的全局变量。由于Python实习标识符和限定常量,您最终会对标识符及其值使用相同的字符串对象:
>>> compile("so = 'so'", '<stdin>', 'single').co_names[0] is compile("so = 'so'", '<stdin>', 'single').co_consts[0]
True

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

>>> some_var = 'Look ma, spaces and punctuation!'
>>> some_other_var = 'Look ma, spaces and punctuation!'
>>> id(some_var)
4493058384
>>> id(some_other_var)
4493058456
>>> foo = 'Concatenating_' + 'also_helps_if_long_enough'
>>> bar = 'Concatenating_' + 'also_helps_if_long_enough'
>>> foo is bar
False
>>> foo == bar
True

Python编译器要么使用窥视孔优化器(Python版本<3.7)或功能更强大的AST优化器(3.7及更新版本)来预计算(折叠)涉及常量的简单表达式的结果。窥视器将其输出限制为长度为20或更少的序列(以防止代码对象膨胀和内存使用),而AST优化器对长度为4096个字符的字符串使用单独的限制。这意味着,如果结果字符串符合当前Python版本的优化器限制,则仅由名称字符组成的较短字符串连接仍然可以导致内部字符串。

。在Python 3.7上,'foo' * 20将产生一个单独的内部字符串,因为常量折叠将其变成一个单独的值,而在Python 3.6或更早的版本上,只有'foo' * 6会被折叠:

>>> import dis, sys
>>> sys.version_info
sys.version_info(major=3, minor=7, micro=4, releaselevel='final', serial=0)
>>> dis.dis("'foo' * 20")
  1           0 LOAD_CONST               0 ('foofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoofoo')
              2 RETURN_VALUE

>>> dis.dis("'foo' * 6")
  1           0 LOAD_CONST               2 ('foofoofoofoofoofoo')
              2 RETURN_VALUE
>>> dis.dis("'foo' * 7")
  1           0 LOAD_CONST               0 ('foo')
              2 LOAD_CONST               1 (7)
              4 BINARY_MULTIPLY
              6 RETURN_VALUE

此行为特定于Python交互式shell。如果我将以下内容放入.py文件中:

print id('so')
print id('so')
print id('so')

并执行它,我收到以下输出:<>之前288896028889602888960之前

在CPython中,字符串被视为常量,我们可以在上面代码片段的字节码中看到:

  2           0 LOAD_GLOBAL              0 (id)
              3 LOAD_CONST               1 ('so')
              6 CALL_FUNCTION            1
              9 PRINT_ITEM          
             10 PRINT_NEWLINE       
  3          11 LOAD_GLOBAL              0 (id)
             14 LOAD_CONST               1 ('so')
             17 CALL_FUNCTION            1
             20 PRINT_ITEM          
             21 PRINT_NEWLINE       
  4          22 LOAD_GLOBAL              0 (id)
             25 LOAD_CONST               1 ('so')
             28 CALL_FUNCTION            1
             31 PRINT_ITEM          
             32 PRINT_NEWLINE       
             33 LOAD_CONST               0 (None)
             36 RETURN_VALUE  

相同的常量(即相同的字符串对象)被加载3次,所以id是相同的。

在第一个示例中,每次都创建字符串'so'的新实例,因此id不同。

在第二个示例中,您将字符串绑定到一个变量,然后Python可以维护字符串的共享副本。

理解这种行为的一种更简单的方法是检查以下数据类型和变量。

"A String peculality"部分用特殊字符举例说明了你的问题。

因此,虽然Python不保证内部字符串,但它会经常重用相同的字符串,is可能会误导。重要的是要知道,你不应该检查idis的字符串是否相等。

为了演示这一点,我发现了一种至少在Python 2.6中强制生成新字符串的方法:

>>> so = 'so'
>>> new_so = '{0}'.format(so)
>>> so is new_so 
False

,这里有更多的Python探索:

>>> id(so)
102596064
>>> id(new_so)
259679968
>>> so == new_so
True

最新更新