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

47 投票
5 回答
11732 浏览
提问于 2025-04-15 18:22

在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(),这意味着每个字符串"Arizona"等只存储了一次,这很好。
但是如果把这个列表写入磁盘再读回来:同样的列表现在却有N个不同的id(),占用的内存多得多,见下文。

这是怎么回事呢?有没有人能解释一下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 http://stackoverflow.com/questions/2117255/python-deep-getsizeof-list-with-contents

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的内存中(或者说任何程序的内存中),有两种类型的字符串:

  • U字符串,存放在一个独特字符串的缓存(Ucache)中:这些字符串节省内存,如果两个字符串都在Ucache中,比较它们是否相等(a == b)会很快。
  • O字符串,其他的字符串,可能会存储多次。

intern(astring)这个函数会把astring放入Ucache(就像给Alex加1);除此之外,我们对Python如何把O字符串移动到Ucache中一无所知——"a"+"b"是怎么进来的,而不是"ab"呢?
(“来自文件的字符串”这个说法没有意义——我们无法知道。)
总之,Ucache(可能有多个)仍然是个谜。

历史小插曲:SPITBOL在1970年左右就实现了所有字符串的唯一化。

5 个回答

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运算符)。

21

我很怀疑Python在这里的表现和其他很多语言是一样的——它会识别你代码中的字符串常量,并使用一个公共的表来管理这些字符串,但在动态创建字符串时却不适用同样的规则。这是有道理的,因为在你的代码中字符串的数量是有限的(当然,Python也允许你动态执行代码),而在程序运行过程中,你可能会创建大量的字符串。

这个过程通常被称为字符串驻留——实际上,从这个页面来看,Python中也叫做字符串驻留。

49

每个Python语言的实现都可以根据自己的需要来处理不可变对象(比如字符串)的分配方式。也就是说,它们可以选择创建一个新的对象,或者找到一个已经存在的相同对象并多加一个引用,这两种方式在语言的角度来看都是可以的。不过在实际操作中,各种实现会找到一个合理的折中方案:如果找到一个合适的现有对象很简单,那就多加一个引用;如果找这个对象可能会花费很长时间,那就直接创建一个新的对象。

举个例子,在一个函数中多次出现相同的字符串字面量(就是直接写出来的字符串),在我知道的所有实现中,都会采用“对同一个对象多加引用”的策略。因为在构建这个函数的常量池时,避免重复是很快且简单的。但是在不同的函数之间这样做可能会非常耗时,所以实际的实现要么根本不这样做,要么只在一些经过经验判断的情况下才会这样做,这样可以在编译时间(因为要找相同的常量而变慢)和内存消耗(如果不断创建新副本会增加)之间找到一个合理的平衡。

我不知道有哪个Python的实现(或者其他语言,比如Java)会在从文件读取数据时去识别可能的重复项(以便通过多个引用重用一个对象)。这样做似乎并不划算(而且这里你是在付运行时的代价,而不是编译时,所以这个折中就更不吸引人了)。当然,如果你知道(通过应用层面的考虑)这些不可变对象很大并且容易重复,你可以很容易地实现自己的“常量池”策略(intern可以帮助你处理字符串,但对于不可变的元组、大的长整型等,自己实现也不难)。

撰写回答