Python何时为相同的字符串分配新内存?

47

两个具有相同字符的Python字符串,a == b, 可能共享内存,即id(a) == id(b), 也可能在内存中出现两次,即id(a) != id(b)。 尝试

ab = "ab"
print id( ab ), id( "a"+"b" )

在这里,Python认识到新创建的"a"+"b"与已经存在内存中的"ab"是一样的 - 不错。

现在考虑一个N长的状态名称列表 [ "Arizona", "Alaska", "Alaska", "California" ... ] (在我的情况下N ~ 500000)。
我看到50个不同的id() s ⇒ 每个字符串"Arizona" ...只被存储一次,很好。
但是将列表写入磁盘并重新读取它: "相同"的列表现在有了N个不同的id() s,占用更多内存,见下文。

为什么会这样 - 有人能解释Python字符串内存分配吗?

""" when does Python allocate new memory for identical strings ?
    ab = "ab"
    print id( ab ), id( "a"+"b" )  # same !
    list of N names from 50 states: 50 ids, mem ~ 4N + 50S, each string once
    but list > file > mem again: N ids, mem ~ N * (4 + S)
"""

from __future__ import division
from collections import defaultdict
from copy import copy
import cPickle
import random
import sys

states = dict(
AL = "Alabama",
AK = "Alaska",
AZ = "Arizona",
AR = "Arkansas",
CA = "California",
CO = "Colorado",
CT = "Connecticut",
DE = "Delaware",
FL = "Florida",
GA = "Georgia",
)

def nid(alist):
    """ nr distinct ids """
    return "%d ids  %d pickle len" % (
        len( set( map( id, alist ))),
        len( cPickle.dumps( alist, 0 )))  # rough est ?
# cf https://dev59.com/vXI95IYBdhLWcg3w3h_G

N = 10000
exec( "\n".join( sys.argv[1:] ))  # var=val ...
random.seed(1)

    # big list of random names of states --
names = []
for j in xrange(N):
    name = copy( random.choice( states.values() ))
    names.append(name)
print "%d strings in mem:  %s" % (N, nid(names) )  # 10 ids, even with copy()

    # list to a file, back again -- each string is allocated anew
joinsplit = "\n".join(names).split()  # same as > file > mem again
assert joinsplit == names
print "%d strings from a file:  %s" % (N, nid(joinsplit) )

# 10000 strings in mem:  10 ids  42149 pickle len  
# 10000 strings from a file:  10000 ids  188080 pickle len
# Python 2.6.4 mac ppc

添加于1月25日:
在Python(或任何程序的)内存中有两种字符串:

  • Ustrings,保存在Ucache中的唯一字符串:这样可以节省内存,并且如果a和b都在Ucache中,使用==比较会很快
  • Ostrings,其他的字符串,可能被存储多次。

intern(astring)将astring放入Ucache中(Alex +1); 除此之外,我们对Python如何将Ostrings移动到Ucache几乎一无所知 - "ab"之后如何进入"a"+"b"? ("来自文件的字符串"毫无意义 - 没有办法知道。)
简而言之,Ucache(可能有几个)仍然是不清楚的。

历史注释: SPITBOL编译器于1970年左右唯一化所有字符串。

5个回答

49
每个 Python 语言的实现都可以自由地在分配不可变对象(比如字符串)时做出权衡——无论是制作一个新对象还是找到一个已存在的相等对象并使用一个更多的引用,从语言的角度来看都是可以的。当然,在实践中,真实的实现会做出合理的妥协:如果寻找这样的对象很容易和便宜,就使用一个适当的现有对象的一个引用,而如果寻找一个适当的现有对象(可能存在,也可能不存在)的任务看起来可能需要很长时间搜索,就制作一个新的对象。

例如,同一函数中多次出现相同的字符串文字(在我所知道的所有实现中)将使用“新引用到相同对象”的策略,因为在构建该函数的常量池时,很容易避免重复;但是在不同的函数之间这样做可能是一个非常耗时的任务,因此真实的实现要么根本不这样做,要么只在某些启发式识别的情况下这样做,其中可以希望编译时间(通过搜索相同的现有常数减慢)与内存消耗(如果不断制作常数的新副本,则增加)之间有一个合理的权衡。

我不知道任何 Python (或者其他具有常量字符串的语言,如 Java) 的实现会在读取文件数据时辨析出可能的重复项(通过多个引用重用单个对象)-- 这似乎不是一个很有前途的权衡(而且在这里你将会支付运行时 而不是 编译时 ,所以这个权衡甚至更不具有吸引力)。当然,如果你知道(基于应用程序层面的考虑),这样的不可变对象是大的并且容易被多次复制,那么你可以很容易地实现自己的 "常量池" 策略(intern 可以帮助你为字符串做到这一点,但是对于包含不可变项、非常大的长整数等元组等,自己实现也并不难)。


@John,我认为拥有两个观点(我的是从“内部人士的角度”,你的是来自一位经验丰富的程序员,没有特殊的“内部人士的角度”关于Python)是有价值的,因为它可以保持原样 - 不确定是否有一种最佳方法在单个答案中获得相同的“三角测量”! - Alex Martelli
2
Lua总是只有一个特定字符串的实例。这是一个非常简洁的系统:在字符串创建时会有一些开销(实际上非常小),使得所有字符串相等比较都是O(1)指针比较。 - Glenn Maynard
@AlexMartelli intern在Python 3.4中已经不再使用。您提到可以“自己编写”,但我不确定如何做到这一点... - max
1
@max,你可以编写一个工厂函数,使用哈希表(为了提高速度)来存储不可变对象(如字符串、元组等),并返回对现有对象的引用(如果存在),或者对新插入的对象的引用(如果之前不存在)。 - Alex Martelli
1
@max 对于Python 3,internsys模块中:https://docs.python.org/3/library/sys.html。 一般来说,您可以建立一个包含您喜欢的类型对象(例如字典)的数据结构,并执行与intern相同的操作:建立存储/查找方法,该方法将字典中的键作为引用返回。 - nealmcb
显示剩余2条评论

21

我强烈怀疑Python在这里像许多其他语言一样 - 识别源代码中的字符串常量并为其使用一个公共表,但在动态创建字符串时不遵循相同的规则。这是有道理的,因为您的源代码中只会有有限的字符串集合(尽管Python当然允许您动态评估代码),而在程序运行过程中创建大量字符串的可能性要高得多。

这个过程通常称为内部化 - 而根据这个页面 的外观,在Python中也称为内部化。


有什么想法为什么 id("ab") == id("a"+"b") 吗? 你是否同意我们不知道 Python 如何运行 Ucaches? - denis
5
为了完整性:表达式"a"+"b"被静态转换为表达式"ab",然后发现它与另一个字符串相同。这一切都是在编译时发生的。 - Armin Rigo

14

顺便提一句:在Python中了解对象的生命周期非常重要。请注意以下会话:

Python 2.6.4 (r264:75706, Dec 26 2009, 01:03:10) 
[GCC 4.3.4] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> a="a"
>>> b="b"
>>> print id(a+b), id(b+a)
134898720 134898720
>>> print (a+b) is (b+a)
False

你认为通过打印两个不同表达式的ID并指出“它们相等,因此这两个表达式必须相等/等效/相同”是错误的。单行输出不一定意味着其所有内容都是在同一时刻创建和/或共存的。

如果你想知道两个对象是否是同一个对象,请直接询问Python(使用is运算符)。


11
这里正在进行的操作需要一些解释:print id(a+b), id(b+a) 这行代码首先将 "a" 和 "b" 拼接成一个新分配的字符串 "ab",然后将其传递给 id 函数并在不再需要时对其进行解除分配。接着,以同样的方式分配了 "ba",并最终被分配到了与 "ab" 相同的内存位置(CPython 倾向于这样做)。然后将 "ba" 传递给 id 函数,返回相同的结果。但是,在下一行中,"ab" 和 "ba" 都保留下来以传递给 is 运算符,因此它们必须被分配到不同的内存位置。 - javawizard

3
x = 42
y = 42
x == y #True
x is y #True

在这个交互中,X和Y应该是相等的(相同的值),但不是同一对象,因为我们运行了两个不同的字面表达式。然而,由于小整数和字符串被缓存并重用,is告诉我们它们引用同一个单一对象。实际上,如果你真的想深入了解,你可以使用标准sys模块中的getrefcount函数来询问Python有多少引用指向一个对象,它返回对象的引用计数。这种行为反映了Python优化其执行速度模型的众多方式之一。
来源:《Learning Python》

2
我找到了一篇很好的文章来解释CPython的intern行为:http://guilload.com/python-string-interning/ 简而言之:
  1. 在CPython中,字符串对象有一个标志来指示它是否在intern中。
  2. 通过将字符串存储在普通字典中,键和值都是字符串的指针来实现字符串的intern。这仅接受string类。
  3. interning帮助Python减少内存消耗,因为对象可以引用相同的内存地址,并加速比较速度,因为它只需要比较字符串的指针。
  4. Python在编译过程中进行intern,这意味着只有文字字符串(或者可以在编译时计算的字符串,例如'hello' + 'world')
  5. 对于您的问题:仅长度为0或1或仅包含ASCII字母(a-z、A-Z、0-9)的字符串会被intern。
  6. Intern之所以有效,是因为字符串是不可变的,否则就没有意义了。
这是一篇非常好的文章,我强烈建议访问他的网站并查看其他文章,值得我们花时间去了解。

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