从Python3开始,str采用Unicode编码(注意这里不是utf8编码,虽然.py文件的默认编码是utf8)。每个标准Unicode字符占用4个字节。这无疑是一种内存浪费。Unicode表示一个字符集,为了传输方便,衍生出utf8、utf16等编码方案,以节省存储空间。Python在内部以类似的方式存储字符串。Unicode字符串的三种内部表示为了减少内存消耗,Python使用三种不同的单位长度来表示字符串:每个字符1个字节(Latin-1)每个字符2个字节(UCS-2)每个字符4个字节(UCS-4)字符串结构在源代码中定义:union{void*any;Py_UCS1*latin1;Py_UCS2*ucs2;Py_UCS4*ucs4;}数据;/*规范的、最小形式的Unicode缓冲区*/}PyUnicodeObject;如果字符串中的所有字符都在ascii码范围内,那么可以用占用1个字节的Latin-1编码存储。而如果有一个字符串需要占用两个字节(比如汉字),那么整个字符串将以占用2个字节的UCS-2编码存储。从sys.getsizeof函数外部可以看出这一点:如图所示,存储'zh'所需的存储空间比'z'多1个字节,这里h占1个字节;存储'z'Medium'需要比'medium'多2个字节的存储空间,这里z占2个字节。对于大多数自然语言,2字节编码就足够了。但是如果有一段1G的ascii文本加载到内存中,在文本中插入一个emoji表情,那么字符串所需的空间就会扩大到4倍,是不是很惊喜?为什么内部不使用utf8进行编码?最流行的Unicode编码方案,Python内部并不使用它。为什么?这里不得不说一下utf8编码带来的缺点。这种编码方案的每个字符占用的字节长度是变化的,这使得无法随机访问单个字符。比如string[n](使用utf8编码)需要先统计前n个字符长度占用的字节数。所以从O(1)到O(n),这就更不能接受了。因此,Python内部采用定长的方式来存储字符串。字符串驻留机制的另一种节省内存的方法是将一些较短的字符串放入一个池中,在程序创建字符串对象之前检查池中是否有满意的字符串。内部只能驻留长度为20或更短的包含下划线(_)、字母和数字的字符串。驻留是在代码编译时进行的,代码中的以下内容会进行驻留检查:emptystring''andall;变量名;参数名称;字符串常量(代码中定义的所有字符串);字典键;属性名称;驻留机制为重复的字符串节省了大量内存。在内部,字符串驻留池由一个以字符串作为键的全局字典维护:voidPyUnicode_InternInPlace(PyObject**p){PyObject*s=*p;对象*t;如果(s==NULL||!PyUnicode_Check(s))返回;//PyUnicodeObject的类型和状态检查if(!PyUnicode_CheckExact(s))return;如果(PyUnicode_CHECK_INTERNED(s))返回;//为实习生机制创建字典if(interned==NULL){interned=PyDict_New();如果(实习==NULL){PyErr_Clear();/*不要留下异常*/return;}}//对象是否存在于intert=PyDict_SetDefault(interned,s,s);//存在,调整引用计数if(t!=s){Py_INCREF(t);Py_SETREF(*p,t);返回;}/*interned中的两个引用不计入refcnt。释放器会处理这个*/Py_REFCNT(s)-=2;_PyUnicode_STATE(s).interned=SSTATE_INTERNED_MORTAL;}变量interned是全局存储字符串池的字典的变量名interned=PyDict_New(),为了让intern机制字符串不被回收,PyDict_SetDefault(实习,s,s);将字符串设置为key,同时也设置为value,这样string对象的引用计数就会+1两次,这样在程序结束之前,字典中存储的对象永远不会为0,这也是y_REFCNT(s)-=2;的原因将count减2。从函数参数可以看出,还是创建了string对象,内部一直是为string创建对象,但是通过inter机制检查之后,临时创建的string会被destroyed因为引用计数为0,临时变量在内存中短暂存在,然后很快消失。字符串缓冲池Python除了字符串常驻池外,还保存了ascii码中的所有单字符:staticPyObject*unicode_latin1[256]={NULL};如果字符串实际上是字符,则优先从缓冲池中获取:[unicodeobjec.c]PyObject*PyUnicode_DecodeUTF8Stateful(constchar*s,Py_ssize_tsize,constchar*errors,Py_ssize_t*consumed){.../*ASCII相当于Unicode中的前128个序数。*/if(size==1&&(unsignedchar)s[0]<128){returnget_latin1_char((unsignedchar)s[0]);}...}然后通过intern机制保存到intern池中,这样驻留池和缓冲池中,都指向同一个字符串对象。严格来说,这种单字符缓冲池并不是一种节省内存的方案,因为几乎所有从中取出的对象都会保存在缓冲池中。这个解决方案是为了减少字符串对象的创建。总结本文介绍了两种节省内存的选项。字符串的每个字符占用相同的大小,具体取决于字符串中最大的字符。短字符串会放在一个全局字典中,这个字典中的字符串变成单例模式以节省内存。
