关于不可变string的变化的ID

关于strtypes的对象的id (在Python 2.7中)困惑我。 strtypes是不可变的,所以我期望一旦它被创build,它将始终具有相同的id 。 我相信我不会自言自语,所以我会发表一个input和输出序列的例子。

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

与此同时,它一直在变化。 但是,在指向该string的variables之后,情况会发生变化:

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

所以它看起来像冻结了id,一旦一个variables持有这个值。 的确,在del sodel not_soid('so')的输出开始再次改变。

这与(小)整数一样。

我知道在不变性和同一个id之间没有真正的联系; 但是,我正在试图找出这种行为的来源。 我相信熟悉python内部构件的人不会比我感到惊讶,所以我试图达到同样的观点。

更新

试着用不同的string给出不同的结果…

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

现在它平等的…

CPython默认情况下不会内联string,但实际上,Python代码库中的很多地方都会重用已经创build的string对象。 很多Python内部使用intern()函数调用来显式实施Pythonstring,但通常情况下 ,Pythonstring文字每次都会创build一个新的string对象。

Python也可以自由重复使用内存位置,Python也会在编译时通过在代码对象中存储字节码来存储一次不变值。 Python REPL(交互式解释器)还将最近的expression式结果存储在_名称中,这更多地混淆了事物。

因此,你不时看到同样的id。

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

  1. 该行被编译,其中包括为string对象创build一个常量:

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

    这显示存储的常量与编译的字节码; 在这种情况下,一个string'foo'None单独。

  2. 执行时,string从代码常量中加载,而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可能重新使用相同的内存位置来创build新的string对象。 如果您重复此代码,通常会导致打印相同的内存地址。 这取决于你对Python内存做了什么

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

接下来,Python编译器还会将任何存储为常量的Pythonstring实习,只要它是有效的标识符。 Python 代码对象工厂函数PyCode_New将实习任何只包含字母,数字或下划线的string对象:

 /* Intern selected string constants */ for (i = PyTuple_Size(consts); --i >= 0; ) { PyObject *v = PyTuple_GetItem(consts, i); if (!PyString_Check(v)) continue; if (!all_name_chars((unsigned char *)PyString_AS_STRING(v))) continue; PyString_InternInPlace(&PyTuple_GET_ITEM(consts, i)); } 

由于您创build了符合条件的string,所以它们是被禁用的,这就是为什么您会看到'so'string使用相同的ID,即使重新创build并绑定到不同的标识符。

顺便说一句,你的新名字so = 'so'一个string绑定到一个包含相同字符的名字。 换句话说,你正在创造一个全球化的名字和价值是平等的。 由于Python实例化标识符和限定常量,因此最终将同一个string对象用于标识符及其值:

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

如果您创build的string不是代码对象常量,或者包含字母+数字+下划线范围之外的字符,则会看到未被重用的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窥视孔优化器预先计算了简单expression式的结果,但是如果结果是一个长于20的序列,那么输出将被忽略(以防止代码对象和内存的膨胀)。 所以如果连接只包含名称字符的较短的string,如果结果为20个字符或更短,仍然可能导致internedstring。

这个行为是特定于Python交互式shell的。 如果我把以下内容放在一个.py文件中:

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

并执行它,我收到以下输出:

 2888960
 2888960
 2888960

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

  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 

相同的常量(即相同的string对象)被加载3次,所以ID是相同的。

在你的第一个例子中,每次创build一个string'so'的新实例,因此是不同的id。

在第二个例子中,你将string绑定到一个variables,然后Python可以维护一个string的共享副本。

所以,虽然Python不保证实习string,但它会经常重复使用相同的string,可能会误导。 知道你不应该检查id或string是否相等is重要的。

为了certificate这一点,我发现至less在Python 2.6中强制使用一个新string的方法是:

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

这里有更多的Python探索:

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

理解行为的更简单方法是检查以下数据types和variables 。

“string大小写”部分说明了使用特殊字符作为示例的问题。