5.1. 字符串对象
字符串对象是“变长对象”。
5.1.1. Python中的创建
Python中字符串(strs)对象最重要的创建方法为PyUnicode_DecodeUTF8Stateful,如下Python语句最终会调用到PyUnicode_DecodeUTF8Stateful:
a = 'hello b = str('world')
5.1.2. PyUnicode_DecodeUTF8Stateful的C调用栈
词法解析,最终调到PyUnicode_DecodeUTF8Stateful,调用顺序如下:
ast_for_expr
=>ast_for_power
=>ast_for_atom_expr
=>ast_for_atom (case STRING)
=>parsestrplus
=>parsestr
=> PyUnicode_DecodeUTF8Stateful
5.1.3. PyUnicode_DecodeUTF8Stateful源码
PyObject * PyUnicode_DecodeUTF8Stateful(const char *s, Py_ssize_t size, const char *errors, Py_ssize_t *consumed) {
_PyUnicodeWriter writer;
const char *starts = s;
const char *end = s + size;
Py_ssize_t startinpos;
Py_ssize_t endinpos;
const char *errmsg = "";
PyObject *error_handler_obj = NULL;
PyObject *exc = NULL;
_Py_error_handler error_handler = _Py_ERROR_UNKNOWN;
if (size == 0) {
if (consumed)
*cOnsumed= 0;
_Py_RETURN_UNICODE_EMPTY();
}
if (size == 1 && (unsigned char)s[0] <128) {
if (consumed)
*cOnsumed= 1;
return get_latin1_char((unsigned char)s[0]);
}
_PyUnicodeWriter_Init(&writer);
writer.min_length = size;
if (_PyUnicodeWriter_Prepare(&writer, writer.min_length, 127) == -1)
goto onError;
writer.pos = ascii_decode(s, end, writer.data);
s += writer.pos;
while (s
可以看到:
- 空串缓存:空串(unicode_empty)为同一个地址,第二次需要空串时,只是将计数加1,在_PyUnicodeWriter_Finish中实现空串缓存。
static PyObject *unicode_empty = NULL;
#define _Py_INCREF_UNICODE_EMPTY() \ do { \ if (unicode_empty != NULL) \ Py_INCREF(unicode_empty); \ else { \ unicode_empty = PyUnicode_New(0, 0); \ if (unicode_empty != NULL) { \ Py_INCREF(unicode_empty); \ assert(_PyUnicode_CheckConsistency(unicode_empty, 1)); \ } \ } \ } while (0)
#define _Py_RETURN_UNICODE_EMPTY() \ do { \ _Py_INCREF_UNICODE_EMPTY(); \ return unicode_empty; \ } while (0)
static PyObject* unicode_result_ready(PyObject *unicode) {
Py_ssize_t length;
length = PyUnicode_GET_LENGTH(unicode);
if (length == 0) {
if (unicode != unicode_empty) {
Py_DECREF(unicode);
_Py_RETURN_UNICODE_EMPTY();
}
return unicode_empty;
}
if (length == 1) {
void *data = PyUnicode_DATA(unicode);
int kind = PyUnicode_KIND(unicode);
Py_UCS4 ch = PyUnicode_READ(kind, data, 0);
if (ch <256) {
PyObject *latin1_char = unicode_latin1[ch];
if (latin1_char != NULL) {
if (unicode != latin1_char) {
Py_INCREF(latin1_char);
Py_DECREF(unicode);
}
return latin1_char;
}
else {
assert(_PyUnicode_CheckConsistency(unicode, 1));
Py_INCREF(unicode);
unicode_latin1[ch] = unicode;
return unicode;
}
}
}
assert(_PyUnicode_CheckConsistency(unicode, 1));
return unicode;
}
- 字符缓冲池:字符(unicode_latin1)为同一个地址,第二次需要该字符时,只是将计数加1,在get_latin1_char中实现字符缓存。
static PyObject *unicode_latin1[256] = {NULL};
PyObject * PyUnicode_DecodeUTF8Stateful(const char *s, Py_ssize_t size, const char *errors, Py_ssize_t *consumed) {
if (size == 1 && (unsigned char)s[0] <128) {
if (consumed)
*cOnsumed= 1;
return get_latin1_char((unsigned char)s[0]);
}
}
static PyObject* get_latin1_char(unsigned char ch) {
PyObject *unicode = unicode_latin1[ch];
if (!unicode) {
unicode = PyUnicode_New(1, ch);
if (!unicode)
return NULL;
PyUnicode_1BYTE_DATA(unicode)[0] = ch;
assert(_PyUnicode_CheckConsistency(unicode, 1));
unicode_latin1[ch] = unicode;
}
Py_INCREF(unicode);
return unicode;
}
5.2. 常量字符串池
a = 'hello'
b = 'hello'
a is b
由上例可以看出Python对常量字符串做了缓存。缓存的关键性实现在PyUnicode_InternInPlace方法中。
5.2.1. PyUnicode_InternInPlace的C调用堆栈
assemble
=>makecode
=>PyCode_New
=>intern_string_constants
=>PyUnicode_InternInPlace
5.2.2. PyUnicode_InternInPlace源码
static PyObject *interned = NULL;
void PyUnicode_InternInPlace(PyObject **p) {
PyObject *s = *p;
PyObject *t;
#ifdef Py_DEBUG
assert(s != NULL);
assert(_PyUnicode_CHECK(s));
#else
if (s == NULL || !PyUnicode_Check(s))
return;
#endif
if (!PyUnicode_CheckExact(s))
return;
if (PyUnicode_CHECK_INTERNED(s))
return;
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear();
return;
}
}
Py_ALLOW_RECURSION
t = PyDict_SetDefault(interned, s, s);
Py_END_ALLOW_RECURSION if (t == NULL) {
PyErr_Clear();
return;
}
if (t != s) {
Py_INCREF(t);
Py_SETREF(*p, t);
return;
}
Py_REFCNT(s) -= 2;
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
}
其中最关键的方法为PyDict_SetDefault,该方法存在于字典对象dictobject.c中。如果没有相同的key(此处为s),则返回defaultobject(此处也为s),否则如果有相同的key则返回对应的value。所以如果t与s不同,则说明字典中有相应的key,此时将t的计数加1,并且将之前常量字符串的对象指向t。
如此一来,常量字符串的对象地址就一致了,此时s的计数会被消除,如果s的计数为0,则会被释放。值得注意的是,常量字符串的对象每次仍旧会被多分配一次内存,只是如果之前有分配过,且如果此次分配的对象计数为0,则会被释放。
有些情况下(字符串包含非0-9a-zA-Z)不会放到字典里,这时候可以通过sys.intern进行性能优化:
import sys
a = '啊'
b = '啊'
a is b
a = sys.intern('啊')
b = sys.intern('啊')
a is b
具体可以参考:memory - What does python sys.intern do, and when should it be used? - Stack Overflow
5.3. 字符串对象的特性
支持tp_as_number、tp_as_sequence、tp_as_mapping这三种操作。
5.3.1. 数值操作
&unicode_as_number,
5.3.2. 序列操作
&unicode_as_sequence,
static PySequenceMethods unicode_as_sequence = {
(lenfunc) unicode_length,
PyUnicode_Concat,
(ssizeargfunc) unicode_repeat,
(ssizeargfunc) unicode_getitem,
0,
0,
0,
PyUnicode_Contains,
};
因为没有实现PySequenceMethods中的设置方法,所以字符串不可变。
其中:
len('hello')
'hello' + 'wolrd'
多个字符串相加效率低于join,join只分配一次内存;
'hello'*10
效率要高于同个字符串相加;
- unicode_getitem:暂时没有找到相应Python语句;
- PyUnicode_Contains
'h' in 'hello'
5.3.3. 关联操作
&unicode_as_mapping,
static PyMappingMethods unicode_as_mapping = {
(lenfunc)unicode_length,
(binaryfunc)unicode_subscript,
(objobjargproc)0,
};
其中:
test = 'hello world'
test[1]
test[0:5]
test[1]会走unicode_subscript方法的index分支,test[0:5]会走slice分支;
5.3.4. to string
unicode_repr,
(reprfunc) unicode_str,
5.3.5. hash
(hashfunc) unicode_hash,
5.3.6. 比较
PyUnicode_RichCompare,
5.3.7. 内置方法
unicode_methods,
5.4 参考