关于不可变字符串的ID变化
关于Python 2.7中字符串类型(str
)对象的id
,我有点困惑。因为字符串是不可变的,所以我本以为一旦创建,它的id
就应该一直保持不变。可能我表达得不太清楚,所以我来举个例子,看看输入和输出的情况。
>>> id('so')
140614155123888
>>> id('so')
140614155123848
>>> id('so')
140614155123808
在这个过程中,它的id
却一直在变化。不过,当有一个变量指向这个字符串后,情况就变了:
>>> so = 'so'
>>> id('so')
140614155123728
>>> so = 'so'
>>> id(so)
140614155123728
>>> not_so = 'so'
>>> id(not_so)
140614155123728
看起来一旦有变量保存了这个值,id
就“冻结”了。实际上,在执行了del so
和del not_so
之后,id('so')
的输出又开始变化了。
这和(小)整数的行为是不一样的。
我知道不可变性和id
保持不变之间没有真正的联系,但我还是想弄清楚这种行为的原因。我觉得熟悉Python内部机制的人可能不会像我这么惊讶,所以我也想达到同样的理解...
更新
尝试用不同的字符串进行测试,结果却有所不同...
>>> id('hello')
139978087896384
>>> id('hello')
139978087896384
>>> id('hello')
139978087896384
现在它是相等的...
5 个回答
虽然Python并不保证会对字符串进行“内部化”,但它经常会重用相同的字符串,这可能会让你在使用is
时产生误解。因此,了解在比较字符串是否相等时,不应该使用id
或is
是很重要的。
为了演示这一点,我发现了一种在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
要更简单地理解这个行为,可以查看以下这个链接:数据类型和变量。
在“字符串的特殊性”这一部分,用特殊字符作为例子来说明你的问题。
在你的第一个例子中,每次都会创建一个新的字符串 'so'
,所以它们的身份(id)是不同的。
而在第二个例子中,你把这个字符串绑定到一个变量上,这样Python就可以保持一个共享的字符串副本。
这种行为是特定于Python的交互式命令行。如果我把下面的内容放在一个.py文件里:
print id('so')
print id('so')
print id('so')
然后执行它,我会得到以下输出:
2888960 2888960 2888960
在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是一样的。
CPython 默认并不保证会对所有字符串进行“内存重用”,但实际上,Python 的很多地方会重复使用已经创建的字符串对象。很多 Python 的内部实现会使用 sys.intern()
函数 来明确地进行字符串的内存重用,不过,除非你碰到那些特殊情况,否则两个相同的字符串字面量会产生不同的字符串。
Python 也可以自由地“重用”内存位置,并且 Python 会通过在编译时将不可变的“字面量”存储一次来进行优化,这些字面量会和字节码一起存储在代码对象中。Python 的 REPL(交互式解释器)还会把最近的表达式结果存储在 _
这个名字里,这让事情变得更加复杂。
因此,你会时不时看到相同的 ID 出现。
在 REPL 中运行 id(<string literal>)
这一行会经历几个步骤:
这一行会被编译,这包括为字符串对象创建一个常量:
>>> compile("id('foo')", '<stdin>', 'single').co_consts ('foo', None)
这显示了与编译字节码一起存储的常量;在这个例子中是字符串
'foo'
和None
单例。简单的表达式如果产生不可变值,可能会在这个阶段被优化,下面会有关于优化器的说明。在执行时,字符串会从代码常量中加载,
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,代码对象被删除。因此,字符串对象也被删除。
然后,如果你重新运行相同的代码,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 编译器要么使用 peephole 优化器(Python 版本 < 3.7),要么使用更强大的 AST 优化器(3.7 及更新版本)来预先计算(折叠)涉及常量的简单表达式的结果。peephole 优化器将输出限制在长度为 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