CPython-内置string类型
内置string类型关联的对象包括:string对象、string类型对象。PyStringObject结构体用来表示string对象,PyString_Type是string类型对象(PyTypeObject类型)。
与内置int类型不同的是:string对象是变长对象,长度取决于字符串的长度。
与内置int类型相同的是:string对象也是不可变对象,即string对象创建后不可添加/删除字符。
PyStringObject
首先来看PyStringObject结构体:
typedef struct { PyObject_VAR_HEAD long ob_shash; // 字符串hash值(-1表示还未计算) int ob_sstate; // interned状态(0表示未被intern) char ob_sval[1]; // 字符数组(ob_size+1,包含结束符) } PyStringObject;
其中PyObject_VAR_HEAD是可变对象的基础数据部分,其中有ob_size字段表示可变对象的长度;
ob_sval是字符数组,用来存在字符串,以'\0'结尾,所以数组大小是ob_size+1;
ob_shash是字符串的hash值,因为字符串经常会被作为字典(dict)的键(key),不管是脚本中还是Python虚拟机中,使用一个字段来保存hash值避免每次都需要重新计算;
ob_sstate用来保存intern状态,包括SSTATE_NOT_INTERNED、SSTATE_INTERNED_MORTAL、SSTATE_INTERNED_IMMORTAL,后面会讲到。
[Python] >>> s1 = '' >>> sys.getsizeof(s1) 29 # 基础大小(PyStringObject_SIZE) >>> s2 = 'abc' >>> sys.getsizeof(s2) 32
PyString_Type
PyTypeObject PyString_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "str", PyStringObject_SIZE, sizeof(char), string_dealloc, /* tp_dealloc */ (printfunc)string_print, /* tp_print */ (hashfunc)string_hash, /* tp_hash */ string_methods, /* tp_methods */ ... };
PyObjectType中有计算字符串hash值的string_hash函数,有兴趣的可以查看string_hash函数的定义;以及string对象特有的方法string_methods,主要包括:join、split、rsplit、lower、upper、find、index、replace等。
对于Python对象,通常需要关注对象创建、对象销毁、对象操作几部分。
string对象创建
创建string对象需要关注string对象的内存分配、字符数组创建和赋值几个部分。可以直接看string对象创建函数:
PyObject * PyString_FromString(const char *str) { register size_t size; register PyStringObject *op; assert(str != NULL); size = strlen(str); // (1)保证字符串不超长 if (size > PY_SSIZE_T_MAX - PyStringObject_SIZE) { PyErr_SetString(PyExc_OverflowError, "string is too long for a Python string"); return NULL; } // (2)空字符串nullstring if (size == 0 && (op = nullstring) != NULL) { Py_INCREF(op); return (PyObject *)op; } // (3)长度为1的字符串尝试从characters中获取 if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) { Py_INCREF(op); return (PyObject *)op; } // (4)分配PyStringObject对象内存 op = (PyStringObject *)PyObject_MALLOC(PyStringObject_SIZE + size); if (op == NULL) return PyErr_NoMemory(); (void)PyObject_INIT_VAR(op, &PyString_Type, size); op->ob_shash = -1; op->ob_sstate = SSTATE_NOT_INTERNED; // (5)将字符串复制到对象内部的字符数组中 Py_MEMCPY(op->ob_sval, str, size+1); if (size == 0) { // (6)长度为0的字符串进行intern操作 PyObject *t = (PyObject *)op; PyString_InternInPlace(&t); op = (PyStringObject *)t; nullstring = op; Py_INCREF(op); } else if (size == 1) { // (7)长度为1的字符串进行intern操作 PyObject *t = (PyObject *)op; PyString_InternInPlace(&t); op = (PyStringObject *)t; characters[*str & UCHAR_MAX] = op; Py_INCREF(op); } return (PyObject *) op; }
(1)保证字符串不超长。PY_SSIZE_T_MAX在不同设备和系统上值不同,在我的测试设备上(Windows 10)值是2147483647B(约2GB),应该几乎不可能会超值这个最大限制。
(2)空字符串nullstring。所有的空字符串会共享nullstring静态对象(会增加引用计数)。
(3)长度为1的字符串尝试从characters中获取(会增加引用计数)。characters是长度为1的字符串缓冲机制,是256个长度为1的字符串对象数组(UCHAR_MAX + 1),对应于ascii字符集。
static PyStringObject *characters[UCHAR_MAX + 1];
(4)分配PyStringObject对象内存。为string对象分配内存,大小为基础大小(PyStringObject_SIZE) + 字符串长度(size)。
(5)将字符串复制到对象内部的字符数组中。
(6)长度为0的字符串进行intern操作,首次创建nullstring对象,会进行intern操作。
(7)长度为1的字符串进行intern操作,首次创建该长度为1的字符串对象,会进行intern操作。
intern操作实现string对象驻留机制,用于对string对象的共享。
intern机制
字符串对象驻留机制,即共享机制。通过驻留能够避免相同字符串对象的重复创建,仅通过增加引用计数来取用已存在的字符串对象,这一切是通过叫interned的字典来实现。
比如创建100个"abcdefg"字符串,如果不使用intern机制则运行时存在100个相同的string对象,造成内存和执行效率浪费,而使用了intern机制后则只需要存有1个共享的string对象。
先通过Python脚本来观察intern机制,然后再来看intern函数源代码。
>>> s1 = 'a' >>> s2 = 'a' >>> s1 is s2 True # 单字符必interning(PyString_FromString函数中可以看出来) >>> s1 = 'abc' >>> s2 = 'abc' >>> s1 is s2 True # 字母字符串会interning >>> s1 = 'abc!' >>> s2 = 'abc!' >>> s1 is s2 False # 带'!'的字符串不会interning >>> s1 = intern('abc!') >>> s2 = intern('abc!') >>> s1 is s2 True # 显式interning >>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa' True # 隐式interning(窥孔优化) >>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa' False # 未interning(长度超过20未触发窥孔优化) >>> '01234567890123456789012345' is '01234567890123456789012345' True # 隐式interning
intern分为显式和隐式两种,显式即通过调用intern函数执行,隐式则包含不少规则:
<1>长度为0或1的字符串必interning。
<2>常用字符(字母、数字、下划线)组成的字符串会进行interning。含有非常用字符的字符串不会进行interning,比如'!'。
<3>常量折叠(constant folding)是Python中的一种窥孔优化(peephole optimization)技术。
这意味着在编译表达式"a * 20"时会被替换为‘aaaaaaaaaaaaaaaaaaaa’常量以减少运行时消耗。
只有长度小于等于20的字符串才会发生常量折叠,假如"a * 1000000"也进行常量折叠,编译得到的字节码大小会被撑爆。
对于常用字符的检查在all_name_chars函数中,可以看一下其源代码:
#define NAME_CHARS "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz" /* all_name_chars(s): true if all chars in s are valid NAME_CHARS */ static int all_name_chars(PyObject *o) { static char ok_name_char[256]; static const unsigned char *name_chars = (unsigned char *)NAME_CHARS; const unsigned char *s, *e; // (1)创建常用字符判定数组,加快判断速度 if (ok_name_char[*name_chars] == 0) { const unsigned char *p; for (p = name_chars; *p; p++) ok_name_char[*p] = 1; } s = (unsigned char *)PyString_AS_STRING(o); e = s + PyString_GET_SIZE(o); // (2)遍历字符串,判断其所有的字符是否都是常用字符 while (s != e) { if (ok_name_char[*s++] == 0) return 0; } return 1; }
再回到PyString_FromString函数中调用的PyString_InternInPlace函数(实现intern机制),其源代码如下:
void PyString_InternInPlace(PyObject **p) { register PyStringObject *s = (PyStringObject *)(*p); PyObject *t; // (1)只对string对象使用intern机制 if (s == NULL || !PyString_Check(s)) Py_FatalError("PyString_InternInPlace: strings only please!"); // (2)string子类对象不使用intern机制 if (!PyString_CheckExact(s)) return; // (3)string对象已被interned(ob_sstate) if (PyString_CHECK_INTERNED(s)) return; // (4)interned字典未创建,则使用PyDict_New创建 if (interned == NULL) { interned = PyDict_New(); if (interned == NULL) { PyErr_Clear(); /* Don't leave an exception */ return; } } t = PyDict_GetItem(interned, (PyObject *)s); // (5)已被interned,则p指向t,t引用计数加1,*p引用计数减1 if (t) { Py_INCREF(t); Py_SETREF(*p, t); return; } // (6)未被interned,则将s加到interned字典中 if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) { PyErr_Clear(); return; } /* The two references in interned are not counted by refcnt. The string deallocator will take care of this */ // (7)s对象引用计数减2 Py_REFCNT(s) -= 2; PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL; }
(4)interned字典未创建,则使用PyDict_New创建。字典是Python中常用的数据类型(PyDictObject),是关联式容器(以键值对存储),interned字典的键和值都是string对象本身。
(5)已被interned,则p指向t,t引用计数加1,*p引用计数减1。因为原始的string对象(s)不需要了,所以需要减少s的引用计数。
(6)未被interned,则将s加到interned字典中。这里隐藏了一条信息:interned字典的键和值都是string对象本身,所以在添加的时候会被分别增加引用计数,即加2。
(7)s对象引用计数减2。与6对应,需要将s引用计数减2以保证能够正确销毁s,这里还设置了string对象的ob_sstate字段(SSTATE_INTERNED_MORTAL),在string对象销毁处会讲解一下它的用处。
注意:intern机制尽管能够通过共享来减少相同string对象的数目,但在intern过程中不能够减少string对象创建,s对象就是先创建出来再来对它进行intern操作。
string对象销毁
在string对象引用计数为0时调用PyString_Type对象中的tp_dealloc字段(string_dealloc函数)进行销毁:
static void string_dealloc(PyObject *op) { switch (PyString_CHECK_INTERNED(op)) { // (1)未被interned的字符串对象直接销毁 case SSTATE_NOT_INTERNED: break; // (2)已被interned的字符串对象(SSTATE_INTERNED_MORTAL),从interned字典中删除后再销毁 case SSTATE_INTERNED_MORTAL: /* revive dead object temporarily for DelItem */ Py_REFCNT(op) = 3; if (PyDict_DelItem(interned, op) != 0) Py_FatalError( "deletion of interned string failed"); break; // (3)已被interned的字符串对象(SSTATE_INTERNED_IMMORTAL)不可被销毁 case SSTATE_INTERNED_IMMORTAL: Py_FatalError("Immortal interned string died."); default: Py_FatalError("Inconsistent interned string state."); } // (4)释放string对象内存 Py_TYPE(op)->tp_free(op); }
(1)未被interned的字符串对象直接销毁。
(2)已被interned的字符串对象(SSTATE_INTERNED_MORTAL),从interned字典中删除后再销毁。因为键和值都是字符串对象本身,所以会对引用计数减2,同时又需要在从字典中删除时不销毁(第4步来销毁),所以把op->ob_refcnt设置为3。
(3)已被interned的字符串对象(SSTATE_INTERNED_IMMORTAL)不可被销毁。SSTATE_INTERNED_IMMORTAL状态的字符串对象的创建过程和SSTATE_INTERNED_MORTAL状态的字符串对象一样,只是在创建后修改ob_sstate状态,变为永久intern字符串对象。
(4)释放字符串对象内存。

浙公网安备 33010602011771号