关于不可变string的变化的ID
关于str
types的对象的id
(在Python 2.7中)困惑我。 str
types是不可变的,所以我期望一旦它被创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 so
和del not_so
, id('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>)
经过几个步骤:
-
该行被编译,其中包括为string对象创build一个常量:
>>> compile("id('foo')", '<stdin>', 'single').co_consts ('foo', None)
这显示存储的常量与编译的字节码; 在这种情况下,一个string
'foo'
和None
单独。 -
执行时,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
-
代码对象不被任何东西引用,引用计数下降到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大小写”部分说明了使用特殊字符作为示例的问题。