Python中未分配的字符串如何在内存中拥有地址?

46

有人能解释一下吗?所以我一直在使用Python中的id()命令,并发现了这个:

>>> id('cat')
5181152
>>> a = 'cat'
>>> b = 'cat'
>>> id(a)
5181152
>>> id(b)
5181152

除了一个问题,这对我来说很有意义:在将字符串“cat”赋给变量之前,它在内存中已经有一个地址。我可能只是不理解内存寻址的工作原理,但有人可以向我解释一下吗?或者至少告诉我应该阅读关于内存寻址的内容吗?

所以这一切都很好,但这让我更困惑了:

>>> a = a[0:2]+'t'
>>> a
'cat'
>>> id(a)
39964224
>>> id('cat')
5181152

这让我感到奇怪,因为'cat'是一个地址为5181152的字符串,但新的a有一个不同的地址。如果内存中有两个'cat'字符串,为什么id('cat')没有打印出两个地址呢?我的最后想法是连接可能与地址的改变有关,所以我尝试了这个:

>>> id(b[0:2]+'t')
39921024
>>> b = b[0:2]+'t'
>>> b
'cat'
>>> id(b)
40000896

我本来会预测这些ID是一样的,但事实并非如此。有什么想法吗?


10
请参见 https://dev59.com/PHE85IYBdhLWcg3w_Itr。 - Zan Lynx
8
"intern" 是 Python 内置函数之一,它用于将字符串对象添加到内部缓存中以实现更有效的字符串比较。当需要频繁比较相同的字符串时,使用 "intern" 可以提高程序的性能。 - Zan Lynx
8
以及https://dev59.com/kkrSa4cB1Zd3GeqPThpg - Zan Lynx
6
更多信息请参见:https://dev59.com/dHI_5IYBdhLWcg3wJfkM - Zan Lynx
7
哇!http://stackoverflow.com/questions/1216259/comparing-two-strings-with-is-not-performing-as-expected(该链接指向一个问题,需要更多上下文才能提供准确的翻译) - Zan Lynx
5个回答

53
Python会非常积极地重复使用字符串文本。它这样做的规则是依赖于实现的,但CPython使用了两种我知道的方法:
- 只包含Python标识符可用字符的字符串被“interned”,这意味着它们被存储在一个大表中并且无论出现在何处都可以重复使用。因此,无论在哪里使用“cat”,它始终指向相同的字符串对象。 - 同一代码块中的字符串文本无论内容和长度如何都会被重复使用。如果您在一个函数中两次放置了完整的葛底斯堡演说的字符串文本,则两次都是相同的字符串对象。在单独的函数中,它们是不同的对象。
这两个优化都在编译时进行(也就是生成字节码时)。
另一方面,类似于chr(99) + chr(97) + chr(116)这样的是一个字符串表达式,它评估为字符串"cat"。在像Python这样的动态语言中,它的值不能在编译时知道(chr()是一个内置函数,但您可能已经重新分配了它),因此通常不会被 interned。因此它的id()"cat"的不同。但是,您可以使用intern()函数强制一个字符串被 interned。因此:
id(intern(chr(99) + chr(97) + chr(116))) == id("cat")   # True

正如其他人提到的,字符串是不可变的,所以可以进行interning。换句话说,无法将"cat"更改为"dog"。您必须生成一个新的字符串对象,这意味着指向相同字符串的其他名称不会受到影响。
顺便提一下,Python还会在编译时将仅包含常量(例如"c" + "a" + "t")的表达式转换为常量,如下面的反汇编所示。根据上述规则,这些将被优化为指向相同的字符串对象。
>>> def foo(): "c" + "a" + "t"
...
>>> from dis import dis; dis(foo)
  1           0 LOAD_CONST               5 ('cat')
              3 POP_TOP
              4 LOAD_CONST               0 (None)
              7 RETURN_VALUE

2
哇,恭喜恭喜,那些金徽章可不容易获得!另外,我尝试了一个格言堡演说的字符串文字,Python 将其 interned,所以我相信它非常积极地这样做。 - kindall
Python并不会将所有字符串字面值进行内部化。哪些被内部化是实现细节,但我相信行为是对只包含可能出现在Python标识符中的字符的字符串字面值进行内部化。如果看起来像葛底斯堡演说已经内部化,那很可能是一个无关但非常相似的优化 - user2357112
那非常有趣! - kindall

48

'cat' 有一个地址,因为你创建它是为了将其传递给 id()。尽管你还没有将它绑定到名称上,但该对象仍然存在。

Python 缓存并重用短字符串。但如果通过串联组装字符串,则绕过搜索缓存并尝试重用的代码。

请注意,字符串缓存的内部工作原理是纯实现细节,不应依赖它。


17

所有的值都必须存储在内存中。这就是为什么id('cat')会产生一个值。你把它称为“不存在”的字符串,但它显然存在,只是还没有被赋给一个名称。

字符串是不可变的,所以解释器可以做一些聪明的事情,比如使所有字面上的 'cat' 实例成为同一个对象,这样id(a)id(b)就是相同的。

对字符串进行操作将产生新的字符串。这些新的字符串可能与先前具有相同内容的字符串相同,也可能不同。

请注意,这些细节都是CPython的实现细节,它们随时可能会改变。在实际编写程序时,您无需关注这些问题。


8

Python变量与其他语言(例如C)的变量有很大不同。

在许多其他语言中,变量是内存中位置的名称。在这些语言中,不同类型的变量可以指向不同类型的位置,并且相同的位置可能会被赋予多个名称。在大多数情况下,给定的内存位置可以随时更改数据。还有一些间接引用内存位置的方式(例如int *p会包含地址,并且在该地址处的内存位置中有一个整数)。但实际上,变量所引用的位置不能更改;变量就是位置。在这些语言中,变量分配实际上是“查找此变量的位置,并将此数据复制到该位置”。

Python的工作方式与此不同。在python中,实际对象放入某些内存位置中,而变量则像位置的标签。Python以一种与变量管理方式不同的方式管理存储的值。在python中,赋值实际上意味着“查找此变量的信息,忘记它已经引用的位置,并将其替换为此新位置”。没有数据被复制。

像python这样工作的语言的一个常见特征(与我们之前讨论的第一种语言相反)是某些类型的对象以特殊方式进行管理;相同的值被缓存,以便它们不占用额外的内存,并且可以非常容易地进行比较(如果它们具有相同的地址,则它们是相等的)。这个过程称为interning;所有python字符串字面值都被缓存(除了一些其他类型),尽管动态创建的字符串可能不会。

在您的确切代码中,语义对话应该是:

# before anything, since 'cat' is a literal constant, add it to the intern cache
>>> id('cat') # grab the constant 'cat' from the intern cache and look up 
              # it's address
5181152
>>> a = 'cat' # grab the constant 'cat' from the intern cache and 
              # make the variable "a" point to it's location 
>>> b = 'cat' # do the same thing with the variable "b"
>>> id(a) # look up the object "a" currently points to, 
          # then look up that object's address
5181152
>>> id(b) # look up the object "b" currently points to, 
          # then look up that object's address
5181152

1
你发布的代码会创建新的字符串作为中间对象。这些创建的字符串最终将与原始字符串具有相同的内容。在中间时间段内,它们不完全匹配原始字符串,并且必须保持在不同的地址。
>>> id('cat')
5181152

正如其他人所回答的那样,通过发出这些指令,您会导致Python虚拟机创建一个包含字符串“cat”的字符串对象。该字符串对象被缓存,并位于地址5181152处。

>>> a = 'cat'
>>> id(a)
5181152

再次强调,变量a已被赋值为指向缓存字符串对象的地址5181152,该对象包含字符串"cat"。

>>> a = a[0:2]
>>> id(a)
27731511

在我修改过的你的程序的这一点上,你已经创建了两个小的字符串对象:'cat''ca''cat'仍然存在于缓存中。a所指向的字符串是一个不同的、可能是新的字符串对象,其中包含字符'ca'
>>> a = a + 't'
>>> id(a)
39964224

现在你已经创建了另一个新的字符串对象。该对象是地址为27731511的字符串'ca'和字符串't'的连接。这个连接与之前缓存的字符串'cat'匹配。Python不会自动检测到这种情况。正如kindall所指出的,您可以使用intern()方法强制搜索。

希望这个解释阐明了a的地址改变的步骤。

您的代码没有包括将a分配为字符串'ca'的中间状态。答案仍然适用,因为Python解释器确实生成一个新的字符串对象来保存中间结果a[0:2],无论您是否将该中间结果分配给变量。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接