不少编程语言中的字符串
都是有字符数组(又称字节序列)
来表示,比如C:
1 char msg[] = "Hello World!" ;
由于1 Byte
占8bit
空间,具有265种排列方式,因此单个字节的表现范围有限,对于英文字符以及常用的符号字符可能够,但为了兼容其他字符(伟大的汉字),计算机的先驱们发明了多字节编码
– 通过多个字节来表示一个字符。
ps:这让我联想起python中实现大整数的方式了–串联多个C整型来表达。
Python提供的解决方案是Unicode字符串(str)对象
,unicode 可以表示各种字符,无需关心编码。
然而,存储或者网络通讯时,字符串对象无法避免的需要序列化
成字节序列进行传输。为此,python额外提供了字节序列对象--bytes
。
简单理解就是和用户做交互的是str对象,是所容易理解的。而和机器做交互则是越简单越好,因此是序列化后的字位码
本节所探讨的是python中的bytes对象
bytes对象定义
python中的bytes对象定义在/Objects/bytesobject.h
1 2 3 4 5 6 7 8 9 10 11 12 13 #ifndef Py_LIMITED_API typedef struct { PyObject_VAR_HEAD Py_hash_t ob_shash; char ob_sval[1 ]; } PyBytesObject; #endif
Py_LIMITED_API
查到是和python版本挂钩的东西,这里不管。
可以看到PyBytesObject
是变长对象,除了变长对象的共有头部PyObject_VAR_HEAD
外,还提供了存放对象hash值结果的ob_shash
字段,再就是存放bytes对象内容的ob_sval
字符数组了。
从上述定义中相关的描述,我们还可以知道:
ob_sval
包含了PyObject_VAR_HEAD.ob_size
长度+1个元素,且最后一个内容存放0,标志结束
ob_shash
使用来存放对象hash计算值的,当为-1时,说明尚未进行对象hash计算。之所以设置这个字段是由于python对象哈希值的应用范围很大,比如dict字典对象以来对象哈希值进行存储,由于便利计算bytes对象哈希值需要遍历其内部的字符数组,开销相对大,因此python选择将哈希值保存起来,以空间换时间,避免重复计算。(smart)
1 2 3 4 import syssys.getsizeof(b'' ) 33
从对象结构来回答这问题:
python在分配bytes对象空间对于ob_sval的元素会多分配一个,用于存放b'00'
,头部及ob_shash的每个字段占据8字节(64位系统下),因此大小为33
创建也是类似之前探讨过的对象:
bytes对象行为
之前我们学习到探讨对象行为 ,需要从对象的类型 出发,这里如出一辙,找到PyBytes_Type
,定位到代码位置:/Objects/bytesobject.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 PyTypeObject PyBytes_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0 ) "bytes" , PyBytesObject_SIZE, sizeof (char ), bytes_dealloc, 0 , 0 , 0 , 0 , (reprfunc)bytes_repr, &bytes_as_number, &bytes_as_sequence, &bytes_as_mapping, (hashfunc)bytes_hash, 0 , bytes_str, PyObject_GenericGetAttr, 0 , &bytes_as_buffer, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_BYTES_SUBCLASS, bytes_doc, 0 , 0 , (richcmpfunc)bytes_richcompare, 0 , bytes_iter, 0 , bytes_methods, 0 , 0 , &PyBaseObject_Type, 0 , 0 , 0 , 0 , 0 , 0 , bytes_new, PyObject_Del, };
从上面的PyBytes_Type
构成,可以看出PyBytesObject
支持的操作类型有数值型操作、序列型操作、关系型操作。
数值型操作
跟踪bytes_as_number
字段,可以看到bytes对象
支持的数值型操作
1 2 3 4 5 6 static PyNumberMethods bytes_as_number = { 0 , 0 , 0 , bytes_mod, };
bytes对象
竟然可以进行数值型操作?!跟进查看:bytes_mod
1 2 3 4 5 6 7 8 9 static PyObject *bytes_mod (PyObject *self, PyObject *arg) { if (!PyBytes_Check(self)) { Py_RETURN_NOTIMPLEMENTED; } return _PyBytes_FormatEx(PyBytes_AS_STRING(self), PyBytes_GET_SIZE(self), arg, 0 ); }
关键是_PyBytes_FormatEx
,从名字上看,是做了类C的格式化输出。
序列型操作
跟踪bytes_as_sequence
来到:
1 2 3 4 5 6 7 8 9 10 static PySequenceMethods bytes_as_sequence = { (lenfunc)bytes_length, (binaryfunc)bytes_concat, (ssizeargfunc)bytes_repeat, (ssizeargfunc)bytes_item, 0 , 0 , 0 , (objobjproc)bytes_contains };
相应字段不为空的则说明相应操作存在,比如bytes_length
,跟进发现定义位置:
1 2 3 4 5 static Py_ssize_tbytes_length (PyBytesObject *a) { return Py_SIZE(a); }
可以看到,是通过返回PyVarObject.ob_size
来获取bytes对象的长度的.
再比如bytes_concat
初看API名称,应该猜得出大概是bytes对象追加内容的功能,顺藤摸瓜
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 static PyObject *bytes_concat (PyObject *a, PyObject *b) { Py_buffer va, vb; PyObject *result = NULL ; va.len = -1 ; vb.len = -1 ; if (PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) != 0 || PyObject_GetBuffer(b, &vb, PyBUF_SIMPLE) != 0 ) { PyErr_Format(PyExc_TypeError, "can't concat %.100s to %.100s" , Py_TYPE(b)->tp_name, Py_TYPE(a)->tp_name); goto done; } if (va.len == 0 && PyBytes_CheckExact(b)) { result = b; Py_INCREF(result); goto done; } if (vb.len == 0 && PyBytes_CheckExact(a)) { result = a; Py_INCREF(result); goto done; } if (va.len > PY_SSIZE_T_MAX - vb.len) { PyErr_NoMemory(); goto done; } result = PyBytes_FromStringAndSize(NULL , va.len + vb.len); if (result != NULL ) { memcpy (PyBytes_AS_STRING(result), va.buf, va.len); memcpy (PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len); } done: if (va.len != -1 ) PyBuffer_Release(&va); if (vb.len != -1 ) PyBuffer_Release(&vb); return result; }
第15行,之前完成的事两个临时对象的申请,规模同传递进来的两个操作对象,并填充进临时对象
第18-22行,当第一个操作对象va的长度为0,则函数执行结果为对象vb,进行返回
第23-27行,类似上面的过程。
第29-32行,判断va是否有充足的空间进行追加,不够则返回错误
第34-38行,重新申请足够大的bytes对象,逐一完成对象va vb内容的拷贝
以上就是bytes_concat
实现的细节了。至于bytes对象的其他序列型操作,读者可自行分析。
关联型操作
跟踪bytes_as_mapping
字段,来到
1 2 3 4 5 static PyMappingMethods bytes_as_mapping = { (lenfunc)bytes_length, (binaryfunc)bytes_subscript, 0 , };
可以看到,支持的操作,第一个先前分析过的bytes_length
还支持bytes_subscript
数据拷贝的陷阱
考虑一下三个PyBytesObject的合并(concat)
实际上在python内部运行如下:
1 2 tmp = a + b result = tmp + c
类比的发现,随着待合并的内容越多,则开销会增大。(只有最后一个对象不会重复拷贝,其余对象会拷贝n-1次)
使用join方法实现
join方法对数据拷贝进行了优化:现遍历待合并对象,得到所有对象的ob_size
,计算总长度。根据这个值创建长度合适的result对象存放结果,再遍历待合并对象,逐一拷贝数据。
字符缓冲池
为了优化单字节bytes对象
的创建效率,python内部维护了一个字符缓冲池,不同于小整数对象缓冲池的是字符缓冲池一开始为空的,随着运行逐渐添加,相关定义
1 static PyBytesObject *characters[UCHAR_MAX + 1 ];
python内部创建单字节bytes对象时,先检查目标对象是否再缓冲池中。
根据注释内容,找得到相应的bytes对象创建方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
python创建bytes对象使用的是PyBytes_FromString
`PyBytes_FromStringAndSize`
先看第一个PyBytes_FromString
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 PyObject * PyBytes_FromString (const char *str) { size_t size; PyBytesObject *op; assert(str != NULL ); size = strlen (str); if (size > PY_SSIZE_T_MAX - PyBytesObject_SIZE) { PyErr_SetString(PyExc_OverflowError, "byte string is too long" ); return NULL ; } if (size == 0 && (op = nullstring) != NULL ) { #ifdef COUNT_ALLOCS null_strings++; #endif Py_INCREF(op); return (PyObject *)op; } if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL ) { #ifdef COUNT_ALLOCS one_strings++; #endif Py_INCREF(op); return (PyObject *)op; } op = (PyBytesObject *)PyObject_MALLOC(PyBytesObject_SIZE + size); if (op == NULL ) return PyErr_NoMemory(); (void )PyObject_INIT_VAR(op, &PyBytes_Type, size); op->ob_shash = -1 ; memcpy (op->ob_sval, str, size+1 ); if (size == 0 ) { nullstring = op; Py_INCREF(op); } else if (size == 1 ) { characters[*str & UCHAR_MAX] = op; Py_INCREF(op); } return (PyObject *) op; }
可以看到第21-27行,就是当创建的bytes对象是单字节,且在单字节缓冲池中,则直接添加缓冲池中目标的引用计数,随后返回。one_strings
是用于计数当前单字节缓冲池的使用情况
在看第二个PyBytes_FromStringAndSize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 PyObject * PyBytes_FromStringAndSize (const char *str, Py_ssize_t size) { PyBytesObject *op; if (size < 0 ) { PyErr_SetString(PyExc_SystemError, "Negative size passed to PyBytes_FromStringAndSize" ); return NULL ; } if (size == 1 && str != NULL && (op = characters[*str & UCHAR_MAX]) != NULL ) { #ifdef COUNT_ALLOCS one_strings++; #endif Py_INCREF(op); return (PyObject *)op; } op = (PyBytesObject *)_PyBytes_FromSize(size, 0 ); if (op == NULL ) return NULL ; if (str == NULL ) return (PyObject *) op; memcpy (op->ob_sval, str, size); if (size == 1 ) { characters[*str & UCHAR_MAX] = op; Py_INCREF(op); } return (PyObject *) op; }
第10-18行,对于单字节对象的创建类似。其中关键的语句是
if (size == 1 && str != NULL &&(op = characters[*str & UCHAR_MAX]) != NULL)
解释:当创建的是单字节对象 && 内容是不为NULL && 内容不在单字节缓冲池中。
疑问
但貌似没看到放入缓冲池的操作,先留个疑问吧。从代码上看貌似只是将缓冲池使用计数的one_strings++
而已,后续在此分配怎么直接引用的?hash?
有没有老哥对于这点有思路的?
好吧,自己画了遍下边的示意图:
发现貌似自己能解答这个问题了(个人理解,可能存在错误),也不修改了,当作个纪念,告诉自己,勤动手才是正确的学习方法。
我们重新贴一遍单bytes对象创建的关键代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (size == 1 && str != NULL && (op = characters[*str & UCHAR_MAX]) != NULL ){ #ifdef COUNT_ALLOCS one_strings++; #endif Py_INCREF(op); return (PyObject *)op; } op = (PyBytesObject *)_PyBytes_FromSize(size, 0 ); if (op == NULL ) return NULL ; if (str == NULL ) return (PyObject *) op; memcpy (op->ob_sval, str, size);if (size == 1 ) { characters[*str & UCHAR_MAX] = op; Py_INCREF(op); }
one_strings
是用于计数当前单字节缓冲池中的使用情况。python初始该值为0,随着运行的进行,每当单字节缓冲池中的内容被使用,则该计数+1,以上图为例,假设标记出的几个被使用了,且其他为NULL,则one_strings = 1
,为什么? ,先留着疑问,我们往下看:
op = characters[*str & UCHAR_MAX]) != NULL
由于单字节缓冲池中的数量是0-255(映射为ascii码)当创建的是单字节,就尝试进行与操作characters[*str & UCHAR_MAX]
,取出相应位置
精彩面试题
1 2 3 4 5 6 7 8 9 >>> a1 = b' a' >>> a2 = b' a' >>> a1 is a2 True >>> ab1 = b' ab' >>> ab2 = b' ab' >>> ab1 is ab2 False
导致上述现象存在的原因是由于python单字节缓冲池存在的原因