本文来说一下字符串的操作,字符串支持哪些操作,取决于类型对象str,所以我们来看看str在底层的定义。
PyTypeObject PyUnicode_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"str", /* tp_name */
sizeof(PyUnicodeObject), /* tp_size */
//...
unicode_repr, /* tp_repr */
&unicode_as_number, /* tp_as_number */
&unicode_as_sequence, /* tp_as_sequence */
&unicode_as_mapping, /* tp_as_mapping */
//...
};
首先哈希(unicode_hash)之类的操作肯定是支持的,然后我们关注一下tp_as_number、tp_as_sequence、tp_as_mapping,我们看到三个操作簇居然都满足。
不过有了bytes的经验,我们知道tp_as_number里面实现的函数只有取模,也就是格式化。bytes和str在很多行为上都是相似的,str对象可以编码成bytes对象,bytes对象可以解码成str对象。
我们来看一下这几个操作簇。
//不出我们所料, 只有一个取模
static PyNumberMethods unicode_as_number = {
0, /*nb_add*/
0, /*nb_subtract*/
0, /*nb_multiply*/
unicode_mod, /*nb_remainder*/
};
//我们看到和bytes对象是几乎一样的
//因为str对象和bytes都是不可变的变长对象,并且可以相互转化
//因此它们的行为是高度相似的
static PySequenceMethods unicode_as_sequence = {
(lenfunc) unicode_length, /* sq_length */
PyUnicode_Concat, /* sq_concat */
(ssizeargfunc) unicode_repeat, /* sq_repeat */
(ssizeargfunc) unicode_getitem, /* sq_item */
0, /* sq_slice */
0, /* sq_ass_item */
0, /* sq_ass_slice */
PyUnicode_Contains, /* sq_contains */
};
//也和bytes对象一样
static PyMappingMethods unicode_as_mapping = {
(lenfunc)unicode_length, /* mp_length */
(binaryfunc)unicode_subscript, /* mp_subscript */
(objobjargproc)0, /* mp_ass_subscript */
};
下面我们就通过源码来考察一下。
字符串的相加
字符串相加会执行PyUnicode_Concat这个操作,将两个字符串组合成一个新的字符串。
PyObject *
PyUnicode_Concat(PyObject *left, PyObject *right)
{
//参数left和right显然是指向字符串的指针
//result则是指向相加之后的字符串
PyObject *result;
//还记得这个Py_UCS4吗, 它是相当于一个无符号32位整型
Py_UCS4 maxchar, maxchar2;
//left的长度、right的长度、相加之后的长度
Py_ssize_t left_len, right_len, new_len;
//检测是否是PyUnicodeObject
if (ensure_unicode(left) < 0)
return NULL;
if (!PyUnicode_Check(right)) {
//如果右边不是str对象的话,报错
PyErr_Format(PyExc_TypeError,
"can only concatenate str (not \"%.200s\") to str",
right->ob_type->tp_name);
return NULL;
}
//属性的初始化
//这些都是Python内部做的检测,我们不用太关心
if (PyUnicode_READY(right) < 0)
return NULL;
//这里是快分支
//如果其中一方为空的话,那么直接返回另一方即可
//显然这里的快分支命中率就没那么高了,但还是容易命中的
if (left == unicode_empty)
return PyUnicode_FromObject(right);
if (right == unicode_empty)
return PyUnicode_FromObject(left);
//计算left的长度和right的长度
left_len = PyUnicode_GET_LENGTH(left);
right_len = PyUnicode_GET_LENGTH(right);
//如果相加超过PY_SSIZE_T_MAX,那么会报错
//因为要维护字符串的长度,显然长度是有范围的
//但是几乎不存在字符串的长度会超过PY_SSIZE_T_MAX
if (left_len > PY_SSIZE_T_MAX - right_len) {
PyErr_SetString(PyExc_OverflowError,
"strings are too large to concat");
return NULL;
}
//计算新的长度
new_len = left_len + right_len;
//计算存储单元占用的字节数
maxchar = PyUnicode_MAX_CHAR_VALUE(left);
maxchar2 = PyUnicode_MAX_CHAR_VALUE(right);
//取大的那一方,比如一个是UCS2、一个是UCS4
//那么相加之后肯定会选择UCS4
maxchar = Py_MAX(maxchar, maxchar2);
//通过PyUnicode_New申请能够容纳new_len个宽字符的PyUnicodeObject
//并且字符的存储单元是大的那一方
result = PyUnicode_New(new_len, maxchar);
if (result == NULL)
return NULL;
//将left拷进去
_PyUnicode_FastCopyCharacters(result, 0, left, 0, left_len);
//将right拷进去
_PyUnicode_FastCopyCharacters(result, left_len, right, 0, right_len);
assert(_PyUnicode_CheckConsistency(result, 1));
//返回
return result;
}
我们看到逻辑还是很清晰的,不过和bytes对象不同,字符串没有实现缓冲区。但是在效率上,和bytes对象是一样的,如果有大量的字符串相加,那么效率会非常低下,官方建议仍是通过 join 的方式。
字符串的 join 对应PyUnicode_Join函数,代码比较长,这里就不贴了,但是逻辑很好理解。
就是获取列表或者元组里面的每一个unicode字符串对象的长度,然后加在一起,并取最大的存储单元,然后一次性申请对应的空间,再逐一进行拷贝。所以拷贝是避免不了的,+这种方式导致低效率的主要原因就在于大量临时PyUnicodeObject的创建和销毁。
因此如果我们要拼接大量的PyUnicodeObject,那么使用join列表或者元组的方式;如果数量不多,还是可以使用+的,毕竟维护一个列表也是需要资源的。使用join的方式,只有在PyUnicodeObject的数量非常多的时候,优势才会凸显出来。
字符串也支持索引、切片等操作,当然逻辑和bytes对象是类似的,这里就不说了,可以自己到源码中看一下
字符串的encode操作
在Python里面我们可以调用字符串的encode方法,得到 bytes 对象,那么它在底层是如何实现的呢?
PyObject *
PyUnicode_Encode(const Py_UNICODE *s,
Py_ssize_t size,
const char *encoding,
const char *errors)
{
PyObject *v, *unicode;
//基于宽字符创建PyUnicodeObject
unicode = PyUnicode_FromWideChar(s, size);
if (unicode == NULL)
return NULL;
//编码成bytes对象,指定 encoding和 errors
//这个Python里面的参数是一致的
v = PyUnicode_AsEncodedString(unicode, encoding, errors);
Py_DECREF(unicode);
return v;
}
所以重点就是PyUnicode_AsEncodedString这个函数,这个函数会根据encoding参数的不同,而调用不同的函数。比如指定为utf-8,那么会调用_PyUnicode_AsUTF8String
PyObject *
_PyUnicode_AsUTF8String(PyObject *unicode, const char *errors)
{ //又调用了unicode_encode_utf8
return unicode_encode_utf8(unicode, _Py_ERROR_UNKNOWN, errors);
}
static PyObject *
unicode_encode_utf8(PyObject *unicode, _Py_error_handler error_handler,
const char *errors)
{ //kind的类型,表示使用哪一种编码
enum PyUnicode_Kind kind;
void *data;
Py_ssize_t size;
//unicode必须是一个字符串
if (!PyUnicode_Check(unicode)) {
PyErr_BadArgument();
return NULL;
}
//必须初始化完毕
if (PyUnicode_READY(unicode) == -1)
return NULL;
//如果unicode是PyASCIIObject
//那么直接获取每个字符的ASCII码,创建bytes对象
if (PyUnicode_UTF8(unicode))
return PyBytes_FromStringAndSize(PyUnicode_UTF8(unicode),
PyUnicode_UTF8_LENGTH(unicode));
//如果不是Latin-1编码,那么获取kind、data、size
kind = PyUnicode_KIND(unicode);
data = PyUnicode_DATA(unicode);
size = PyUnicode_GET_LENGTH(unicode);
// 判断 kind 是哪一种
switch (kind) {
default:
Py_UNREACHABLE();
//不同的kind执行不同的逻辑
//最终得到的都是 bytes 对象
case PyUnicode_1BYTE_KIND:
assert(!PyUnicode_IS_ASCII(unicode));
return ucs1lib_utf8_encoder(unicode, data, size, error_handler, errors);
case PyUnicode_2BYTE_KIND:
return ucs2lib_utf8_encoder(unicode, data, size, error_handler, errors);
case PyUnicode_4BYTE_KIND:
return ucs4lib_utf8_encoder(unicode, data, size, error_handler, errors);
}
}
整个过程还是我们所说的,通过utf-8编码将每个字符转成对应的编号,组合起来得到的就是bytes对象。
小结
以上我们就简单介绍了字符串的操作,当然字符串操作还有很多,比如 split、strip、title 等等,有兴趣可以进入源码中查看。看看这些操作,底层是如何使用 C 来实现的,对我们的编码水平也会有很大的帮助。
以上就是本次分享的所有内容,想要了解更多欢迎前往公众号:Python编程学习圈,每日干货分享