热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

pythonbytes操作_《深度剖析CPython解释器》6.解密Python中bytes对象的底层实现,以及相关操作...

楔子不少编程语言中的字符串都是使用字符数组(或者称字符序列)来表示,比如C语言和go语言就是这样。charname[]komeijisatori;一个字节最多

楔子

不少编程语言中的"字符串"都是使用字符数组(或者称字符序列)来表示,比如C语言和go语言就是这样。

char name[] = "komeiji satori";

一个字节最多能表示256个字符,所以对于英文来说足够了,因此一个英文字符占一个字节即可,然而对于那些非英文字符便力不从心了。因此为了表示这些非英文编码,于是多字节编码应运而生----通过多个字节来表示一个字符。但由于原始字节序列不维护编码信息,因此操作不慎便导致各种乱码现象。

而Python提供的解决方案是使用unicode(在Python3中等价于str)表示字符串,因为unicode可以表示各种字符,不需要关心编码的问题。但在存储或网络通讯时,字符串不可避免地要序列化成字节序列。为此,Python除了提供字符串对象之外,还额外提供了字节序列对象----bytes。

如上图,str对象统一表示一个字符串,不需要关心编码;计算机通过字节序列和存储介质、网络介质打交道,字节序列由bytes对象表示;在存储和传输str对象的时候,需要将其序列化成字节序列,序列化也是编码的过程。

下面我们就来看看bytes对象在底层的数据结构。

PyBytesObject

我们说bytes对象是由若干个字节组成的,显然这是一个变长对象,有多少个字节说明其长度是多少。

//Include/bytesobject.h

typedef struct {

PyObject_VAR_HEAD

Py_hash_t ob_shash;

char ob_sval[1];

/* Invariants:

* ob_sval contains space for 'ob_size+1' elements.

* ob_sval[ob_size] == 0.

* ob_shash is the hash of the string or -1 if not computed yet.

*/

} PyBytesObject;

我们看一下里面的成员对象:

PyObject_VAR_HEAD:变长对象的公共头部

ob_shash:保存该字节序列的哈希值,之所以选择保存是因为在很多场景都需要bytes对象的哈希值。而Python在计算字节序列的哈希值的时候,需要遍历每一个字节,因此开销比较大。所以会提前计算一次并保存起来,这样以后就不需要算了,可以直接拿来用,并且bytes对象是不可变的,所以哈希值是不变的。

ob_sval:这个和PyLongObject中的ob_digit的声明方式是类似的,虽然声明的时候长度是1, 但具体是多少则取决于bytes对象的字节数量。这是C语言中定义"变长数组"的技巧, 虽然写的长度是1, 但是你可以当成n来用, n可取任意值。显然这个ob_sval存储的是所有的字节,因此Python中的bytes的值,底层是通过字符数组存储的。而且通过注释,我们发现会多申请一个空间,用于存储\0,因为C中是通过\0来表示一个字符数组的结束,但是计算ob_size的时候不包括\0。

我们创建几个不同的bytes对象,然后通过画图感受一下:

val = b""

我们看到一个空的字节序列,底层的ob_savl也是需要一个'\0'的,那么这个结构体实例占多大内存呢?我们说上面ob_sval之外的四个成员,显然每个都是8字节,而ob_savl每个成员都是一个char、也就是占1字节,所以Python中bytes对象占的内存等于32 + ob_sval的长度。而ob_sval里面至少有一个'\0',因此对于一个空的字节序列,显然占33个字节。注意:ob_size统计的是ob_sval中有效字节的个数,不包括'\0',但是计算占用内存的时候,显然是需要考虑在内的,因为它确实多占用了一个字节的空间。或者说bytes对象占的内存等于33 + ob_size也是可以的。

>>> val = b""

>>> sys.getsizeof(val)

33

>>>

val = b"abc"

>>> val = b"abc"

>>> sys.getsizeof(val)

36 # 32 + 4

>>>

bytes对象的行为

介绍bytes对象在底层的数据结构之后,我们要考察bytes对象的行为。我们说实例对象的行为由其类型对象决定,所以bytes对象具有哪些行为,就看bytes类型对象本身定义了哪些操作。bytes类型对象,显然对应PyBytes_Type,根据我们之前介绍的规律,也可以猜出来,它定义在Object/bytesobject.c中。

PyTypeObject PyBytes_Type = {

PyVarObject_HEAD_INIT(&PyType_Type, 0)

"bytes",

PyBytesObject_SIZE,

sizeof(char),

// ...

&bytes_as_number, /* tp_as_number */

&bytes_as_sequence, /* tp_as_sequence */

&bytes_as_mapping, /* tp_as_mapping */

(hashfunc)bytes_hash, /* tp_hash */

// ...

};

到了现在,相信你对类型对象的结构肯定非常熟悉了,因为类型对象都是由PyTypeObject结构体实例化得到的。我们看到tp_as_number,它居然不是0,而是传递了一个指针,说明确实指向了一个PyNumberMethods结构体实例。难道bytes支持数值运算,这显然是不可能的啊,所以我们需要进入bytes_as_number中一探究竟。

static PyNumberMethods bytes_as_number = {

0, /*nb_add*/

0, /*nb_subtract*/

0, /*nb_multiply*/

bytes_mod, /*nb_remainder*/

}

//我们看到它只定义了一个取模操作,也就是%

//看到%估计有人已经明白了,这是格式化

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);

}

由此可见,bytes对象只是借用了%运算实现了格式化,谈不上数值运算,虚惊一场。不过由此也看到了Python的动态特性,即使是相同的操作,但如果是不同类型的对象执行的话,也会有不同的表现。

>>> info = b"name: %s, age: %d"

>>> info % (b"satori", 16)

b'name: satori, age: 16'

>>>

除了tp_as_number,PyBytes_Type还给tp_as_sequence成员传递了bytes_as_sequence指针,说明bytes对象支持序列操作。显然这是肯定的,而且bytes对象显然是序列型对象,所以序列型操作才是我们的研究的重点,下面看看bytes_as_sequence的定义。

static PySequenceMethods bytes_as_sequence = {

(lenfunc)bytes_length, /*sq_length*/

(binaryfunc)bytes_concat, /*sq_concat*/

(ssizeargfunc)bytes_repeat, /*sq_repeat*/

(ssizeargfunc)bytes_item, /*sq_item*/

0, /*sq_slice*/

0, /*sq_ass_item*/

0, /*sq_ass_slice*/

(objobjproc)bytes_contains /*sq_contains*/

};

根据定义我们看到,bytes对象支持的序列型操作一共有5个:

sq_length:查看序列的长度

sq_concat:将两个序列合并为一个

sq_repeat:将序列重复多次

sq_item:根据索引获取指定的下表, 得到一个整型;如果是切片,那么还会得到一个bytes对象

sq_contains:判断某个序列是不是在该序列中,显然它等价于Python中的in操作

查看序列长度:

显然这是最简单的,直接获取ob_size即可,比如:val = b"abcde",那么长度就是5。

static Py_ssize_t

bytes_length(PyBytesObject *a)

{

return Py_SIZE(a);

}

将两个序列合并为一个:

>>> a = b"abc"

>>> b = b"def"

>>> a + b

b'abcdef'

>>>

而且我们看到这里相当于是加法运算,我们很容易想到会是PyNumberMethods中的nb_add,比如:PyLongObject对应的long_add、PyFloatObject对应的float_add,但对于bytes对象而言,加法操作对应PySequenceMethods的sq_concat。所以我们看到Python中的同一个操作符,在底层会对应不同的函数,比如:long_add和float_add、以及这里的bytes_concat,在Python的层面都是+这个操作符。然后我们看看底层是怎么对两个字节序列进行相加的。

static PyObject *

bytes_concat(PyObject *a, PyObject *b)

{

//两个局部变量,用于维护缓冲区

Py_buffer va, vb;

//result用于保存结果

PyObject *result = NULL;

//将缓冲区的长度设置为-1, 可以认为此时缓冲区啥也没有

va.len = -1;

vb.len = -1;

//将a、b中ob_sval拷贝到缓冲区中,拷贝成功返回0,拷贝失败返回非0

//如果下面的条件不成功, 就意味着拷贝失败了, 说明至少有一个老铁不是bytes类型

if (PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) != 0 ||

PyObject_GetBuffer(b, &vb, PyBUF_SIMPLE) != 0) {

//然后设置异常,PyExc_TypeError表示TypeError(类型错误),专门用来指对一个对象执行了它所不支持的操作

PyErr_Format(PyExc_TypeError, "can't concat %.100s to %.100s",

Py_TYPE(b)->tp_name, Py_TYPE(a)->tp_name);

//比如:"123" + 123, 会得到: TypeError: can't concat int to bytes, 和这里设置的异常信息是一样的

//这里直接跳转到done

goto done;

}

//这里是判断是否有一方长度为0, 如果a长度为0,那么相加之后结果就是b

if (va.len == 0 && PyBytes_CheckExact(b)) {

//将b拷贝给result

result = b;

//增加result的引用计数

Py_INCREF(result);

//跳转

goto done;

}

//和上面同理,如果b长度为0,那么相加之后的结果就是a

if (vb.len == 0 && PyBytes_CheckExact(a)) {

//将a拷贝给result

result = a;

//增加引用计数

Py_INCREF(result);

//跳转

goto done;

}

//这里是判断两个字节序列合并之后,长度是否超过限制,因为不允许超过PY_SSIZE_T_MAX

//所以更直观的写法应该是 if (va.len + vb.len > PY_SSIZE_T_MAX), 但是这个条件基本不可能满足,除非你写恶意代码

if (va.len > PY_SSIZE_T_MAX - vb.len) {

PyErr_NoMemory();

goto done;

}

//否则话,声明指定容量PyBytesObject

result = PyBytes_FromStringAndSize(NULL, va.len + vb.len);

if (result != NULL) {

//将缓冲区va里面内容拷贝到result的ob_sval中,拷贝的长度为va.len

//PyBytes_AS_STRING是一个宏,用于获取PyBytesObject中的ob_sval

memcpy(PyBytes_AS_STRING(result), va.buf, va.len);

//然后将缓冲区vb里面的内容拷贝到result的ob_sval中,拷贝的长度为vb.len,但是从va.len的位置开始拷贝, 不然会把内容覆盖掉

memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);

}

done:

//如果长度不会-1,那么要将缓冲区里面的内容释放掉,否则可能导致内存泄漏

if (va.len != -1)

PyBuffer_Release(&va);

if (vb.len != -1)

PyBuffer_Release(&vb);

//返回result

return result;

}

虽然代码很长,但是不难理解。不过可能有人认为为什么非要先将a、b的内容拷贝到Py_buffer里面,再通过Py_buffer拷贝到result里面去呢?直接拷贝不可以吗?答案是Py_buffer提供了一套操作对象缓冲区的统一接口,屏蔽不同类型对象的内部差异。

将序列重复多次:

>>> a = b"abc"

>>> a * 3

b'abcabcabc'

>>> a * -1

b'' # 如果乘上一个负数,等于乘上0,那么会得到一个空的字节序列

>>>

然后我们看看底层的实现:

static PyObject *

bytes_repeat(PyBytesObject *a, Py_ssize_t n)

{

Py_ssize_t i;

Py_ssize_t j;

Py_ssize_t size;

PyBytesObject *op;

size_t nbytes;

//如果n小于0, 那么等于0

if (n <0)

n &#61; 0;

//这里条件写成Py_SIZE(a) * n > PY_SSIZE_T_MAX更容易理解

if (n > 0 && Py_SIZE(a) > PY_SSIZE_T_MAX / n) {

//先计算相乘之后字节序列的长度是否超过最大限制&#xff0c;如果超过了&#xff0c;直接报错

PyErr_SetString(PyExc_OverflowError,

"repeated bytes are too long");

return NULL;

}

//计算Py_SIZE(a) * n得到size

size &#61; Py_SIZE(a) * n;

if (size &#61;&#61; Py_SIZE(a) && PyBytes_CheckExact(a)) {

//如果两者相等&#xff0c;那么证明n &#61; 1&#xff0c;直接增加引用计数&#xff0c;然后返回a即可

Py_INCREF(a);

return (PyObject *)a;

}

//类型转化&#xff0c;此时是size_t类型&#xff0c;相当于无符号64位整型

nbytes &#61; (size_t)size;

//PyBytesObject_SIZE是一个宏&#xff0c;表示PyBytesObject的基本大小

//它是一个宏&#xff0c;等价于(offsetof(PyBytesObject, ob_sval) &#43; 1), 显然是33

//所以nbytes &#43; PyBytesObject_SIZE就是bytes对象所需要的空间

//如果nbytes &#43; PyBytesObject_SIZE还小于等于nbytes, 所以相加之后size_t类型存不下了

//说明超过所占内存的极限了

if (nbytes &#43; PyBytesObject_SIZE <&#61; nbytes) {

PyErr_SetString(PyExc_OverflowError,

"repeated bytes are too long");

return NULL;

}

//申请空间&#xff0c;大小为PyBytesObject_SIZE &#43; nbytes

op &#61; (PyBytesObject *)PyObject_MALLOC(PyBytesObject_SIZE &#43; nbytes);

if (op &#61;&#61; NULL)

//返回NULL&#xff0c;表示申请失败

return PyErr_NoMemory();

//PyObject_INIT_VAR是一个宏&#xff0c;设置ob_type和ob_size

(void)PyObject_INIT_VAR(op, &PyBytes_Type, size);

//设置ob_shash为-1

op->ob_shash &#61; -1;

//将ob_sval最后一位设置为&#39;\0&#39;

op->ob_sval[size] &#61; &#39;\0&#39;;

if (Py_SIZE(a) &#61;&#61; 1 && n > 0) {

//显然这里是在a对应的bytes对象长度为1时&#xff0c;所走的逻辑

//直接将op->ob_sval里面元素设置a->ob_sval[0], 设置n个

memset(op->ob_sval, a->ob_sval[0] , n);

return (PyObject *) op;

}

i &#61; 0;

//否则将a -> ob_sval拷贝到op -> ob_sval中, 拷贝n次, 因为size &#61; Py_SIZE(a) * n;

//这里是先拷贝了一次

if (i

memcpy(op->ob_sval, a->ob_sval, Py_SIZE(a));

i &#61; Py_SIZE(a);

}

//然后拷贝n - 1次

while (i

j &#61; (i <&#61; size-i) ? i : size-i;

memcpy(op->ob_sval&#43;i, op->ob_sval, j);

i &#43;&#61; j;

}

return (PyObject *) op;

}

根据索引获取指定元素&#xff1a;

>>> val &#61; b"abcdef"

>>> val[1], type(val[1])

(98, )

>>>

>>> val[1: 4], type(val[1:4])

(b&#39;bcd&#39;, )

>>>

然后我们看看底层的实现&#xff1a;

static PyObject *

bytes_item(PyBytesObject *a, Py_ssize_t i)

{

//如果i <0或者 i >&#61; a的ob_size&#xff0c;那么会报错:索引越界

//但是我们记得Python支持负数索引的啊&#xff0c;是的&#xff0c;只不过会手动帮你变成正的

//因为C是不支持负数索引的&#xff0c;所以通过C的索引获取&#xff0c;那么索引一定是正的

//因此我们填上的负数&#xff0c;Python会帮你加上长度。比如&#xff1a;长度为5&#xff0c;但是我们写的索引为-1, 那么Python会帮你变成4之后再获取

if (i <0 || i >&#61; Py_SIZE(a)) {

PyErr_SetString(PyExc_IndexError, "index out of range");

return NULL;

}

//我耳机看到获取第i个元素之后直接转成了PyLongObject&#xff0c;然后返回指针

return PyLong_FromLong((unsigned char)a->ob_sval[i]);

}

那切片呢&#xff1f;切片的话对应bytes_subscript&#xff0c;但它不是在PySequenceMethods tp_as_sequence里面&#xff0c;而是在PyMappingMethods bytes_as_mapping里面&#xff0c;它是一个映射操作。

static PySequenceMethods bytes_as_sequence &#61; {

(lenfunc)bytes_length, /*sq_length*/

(binaryfunc)bytes_concat, /*sq_concat*/

(ssizeargfunc)bytes_repeat, /*sq_repeat*/

(ssizeargfunc)bytes_item, /*sq_item*/

0, /*sq_slice*/

0, /*sq_ass_item*/

0, /*sq_ass_slice*/

(objobjproc)bytes_contains /*sq_contains*/

};

//我们看到映射操作&#xff0c;bytes对象中只有两个&#xff0c;一个bytes_length获取长度&#xff0c;这个在bytes_as_sequence中已经实现了&#xff0c;还有一个就是bytes_subscript进行切片操作

static PyMappingMethods bytes_as_mapping &#61; {

(lenfunc)bytes_length,

(binaryfunc)bytes_subscript,

0,

};

因为映射操作只有两个&#xff0c;一个是重复的&#xff0c;还有一个是必须要在这里说的&#xff0c;所以映射操作我们就放在这里介绍了。

static PyObject*

bytes_subscript(PyBytesObject* self, PyObject* item)

{

//参数是self和item&#xff0c;那么在Python的层面上就类似于self[item]

//检测item&#xff0c;看它是不是一个整型

if (PyIndex_Check(item)) {

//如果是转成Ssize_t

Py_ssize_t i &#61; PyNumber_AsSsize_t(item, PyExc_IndexError);

if (i &#61;&#61; -1 && PyErr_Occurred())

return NULL;

//如果i小于0&#xff0c;那么将i加上序列的长度&#xff0c;得到正数索引

if (i <0)

i &#43;&#61; PyBytes_GET_SIZE(self);

if (i <0 || i >&#61; PyBytes_GET_SIZE(self)) {

PyErr_SetString(PyExc_IndexError,

"index out of range");

return NULL;

}

//得到整型

return PyLong_FromLong((unsigned char)self->ob_sval[i]);

}

//检测是否是一个切片

else if (PySlice_Check(item)) {

//起始、终止、步长、拷贝的字节个数、循环变量

Py_ssize_t start, stop, step, slicelength, i;

size_t cur; //拷贝的字节所在的位置

//两个缓存

char* source_buf;

char* result_buf;

//返回的结果

PyObject* result;

//这里是会将item解包

if (PySlice_Unpack(item, &start, &stop, &step) <0) {

return NULL;

}

//得到拷贝的字节个数比如&#xff1a;ob_sval长度为9, 但是未必拷贝9个&#xff0c;所以这个slicelength是计算的拷贝的字节个数

slicelength &#61; PySlice_AdjustIndices(PyBytes_GET_SIZE(self), &start,

&stop, step);

//slicelength小于等于0的话&#xff0c;直接返回空的字节序列&#xff0c;比如val[3: 2]&#xff0c;显然此时是不循环的&#xff0c;因为start对应的位置在end之后&#xff0c;而且步长为正

if (slicelength <&#61; 0) {

return PyBytes_FromStringAndSize("", 0);

}

//如果起始位置为0&#xff0c;步长为1&#xff0c;且拷贝的字节个数等于字节序列的长度

else if (start &#61;&#61; 0 && step &#61;&#61; 1 &&

slicelength &#61;&#61; PyBytes_GET_SIZE(self) &&

PyBytes_CheckExact(self)) {

//那么增加引用计数&#xff0c;直接返回

Py_INCREF(self);

return (PyObject *)self;

}

else if (step &#61;&#61; 1) {

//如果步长是1&#xff0c;那么从start开始拷贝&#xff0c;拷贝slicelength个字字节

return PyBytes_FromStringAndSize(

PyBytes_AS_STRING(self) &#43; start,

slicelength);

}

else {

//走到这里&#xff0c;说明步长不是1&#xff0c;只能一个一个拷贝了

source_buf &#61; PyBytes_AS_STRING(self);

//创建PyBytesObject对象&#xff0c;空间为slicelength

result &#61; PyBytes_FromStringAndSize(NULL, slicelength);

if (result &#61;&#61; NULL)

return NULL;

//拿到内部的ob_sval

result_buf &#61; PyBytes_AS_STRING(result);

//从start开始然后一个字节一个字节的拷贝过去

//start开始拷贝&#xff0c;依旧循环slicelength&#xff0c;通过cur记录拷贝的位置&#xff0c;然后每次循环都加上步长step

for (cur &#61; start, i &#61; 0; i

cur &#43;&#61; step, i&#43;&#43;) {

result_buf[i] &#61; source_buf[cur];

}

//返回

return result;

}

}

//item要么是整数、要么是切片&#xff0c;走到这里说明不满足条件

else {

//比如&#xff1a;item我们传递了一个字符串&#xff0c;显然此时在通过这种方式获取的话&#xff0c;这属于字典的操作

//所以抛出TypeError异常

PyErr_Format(PyExc_TypeError,

"byte indices must be integers or slices, not %.200s",

Py_TYPE(item)->tp_name);

//返回空

return NULL;

}

}

所以从底层我们可以看到&#xff0c;Python为我们做的事情是真的不少&#xff0c;我们通过一个简单的切片&#xff0c;在底层要这么多行代码。不过在我们分析完逻辑之后&#xff0c;会发现其实也不过如此&#xff0c;毕竟逻辑很好理解。

但是在Python中&#xff0c;索引操作和切片操作&#xff0c;我们都可以通过__getitem__实现。

class A:

def __getitem__(self, item):

return item

a &#61; A()

print(a[123]) # 123

print(a["name"]) # name

print(a[1: 5]) # slice(1, 5, None)

print(a[1: 5: 2]) # slice(1, 5, 2)

print(a["yo": "ha": "哼哼"]) # slice(&#39;yo&#39;, &#39;ha&#39;, &#39;哼哼&#39;)

# 通过__getitem__&#xff0c;我们可以同时实现切片、索引获取&#xff0c;但是当item为字符串时&#xff0c;我们还可以实现字典操作

# 当然这部分内容&#xff0c;我们会在后面系列中分析类的时候介绍。

判断一个序列是否在指定的序列中&#xff1a;

>>> val &#61; b"abcdef"

>>> b"abc" in val

True

>>> b"cbd" in val

False

>>>

如果让你来实现的话&#xff0c;显然是两层for循环&#xff0c;那么Python是怎么做的呢&#xff1f;

static int

bytes_contains(PyObject *self, PyObject *arg)

{

//比如: b"abc" in b"abcde"会调用这里的bytes_contains

//self就是b"abcde"对应的PyBytesObject的指针,arg是b"abc"对应的PyBytesObject的指针

//显然这里调用了_Py_bytes_contains, 传入了self -> ob_sval, self -> ob_size, arg

return _Py_bytes_contains(PyBytes_AS_STRING(self), PyBytes_GET_SIZE(self), arg);

}

//上面的源码没有说明&#xff0c;显然是在bytesobject.c中

//但是_Py_bytes_contains位于bytes_methods.c中

_Py_bytes_contains(const char *str, Py_ssize_t len, PyObject *arg)

{

//将arg转成整型, 但是显然只有当arg -> ob_savl的有效字节为1时才可以这么做

Py_ssize_t ival &#61; PyNumber_AsSsize_t(arg, NULL);

if (ival &#61;&#61; -1 && PyErr_Occurred()) {

//所以如果ival &#61;&#61; -1 && PyErr_Occurred()&#xff0c;说明arg -> ob_sval的有效字节数大于1

Py_buffer varg;//缓冲区

Py_ssize_t pos;//遍历位置

PyErr_Clear();//这里将异常清空

//将arg -> ob_sval设置到缓存区中

if (PyObject_GetBuffer(arg, &varg, PyBUF_SIMPLE) !&#61; 0)

return -1;

//调用stringlib_find找到其位置&#xff0c;里面也是使用了循环

pos &#61; stringlib_find(str, len,

varg.buf, varg.len, 0);

PyBuffer_Release(&varg); //释放缓冲区

//如果pos大于0确实找到了&#xff0c;否则返回-1

return pos >&#61; 0;

}

//否则说明字节不合法

if (ival <0 || ival >&#61; 256) {

PyErr_SetString(PyExc_ValueError, "byte must be in range(0, 256)");

return -1;

}

//走到这里说明是单个字节&#xff0c;直接调用C中memchr去寻找即可

return memchr(str, (int) ival, len) !&#61; NULL;

}

效率问题

我们知道Python中对于不可变对象运算的处理方式就是&#xff0c;再创建一个新的。所以三个bytes对象a、b、c相加时&#xff0c;那么会先根据a &#43; b创建新的临时对象&#xff0c;然后再根据"临时对象&#43;c"创建新的对象&#xff0c;返回指针。所以&#xff1a;

result &#61; b""

for _ in bytes_list:

result &#43;&#61; _

这是一种效率非常低下的做法&#xff0c;因为涉及大量临时对象的创建和销毁&#xff0c;不仅是这里bytes&#xff0c;后面即将分析的字符串也是同样的道理。官方推荐的做法是&#xff0c;使用join&#xff0c;字符串和字节序列都可以对一个列表进行join&#xff0c;将列表里面的多个字符串或者字节序列join在一起。

举个Python中的例子&#xff0c;我们以字符串为例&#xff0c;字节序列同样如此&#xff1a;

def bad():

s &#61; ""

for _ in range(1, 10):

s &#43;&#61; str(_)

return s

def good():

l &#61; []

for _ in range(1, 10):

l.append(str(_))

return "".join(l)

def better():

return "".join(str(_) for _ in range(1, 10))

def best():

return "".join(map(str, range(1, 10)))

字节序列缓冲池

为了优化单字节bytes对象的创建效率&#xff0c;Python底层内部维护了一个缓冲池。

static PyBytesObject *characters[UCHAR_MAX &#43; 1];

Python内部创建单字节bytes对象时&#xff0c;先检查目标对象是否已在缓冲池中。PyBytes_FromStringAndSize函数是负责创建bytes对象的通用接口&#xff0c;同样位于 Objects/bytesobject.c 中&#xff1a;

PyObject *

PyBytes_FromStringAndSize(const char *str, Py_ssize_t size)

{

//PyBytesObject对象的指针

PyBytesObject *op;

if (size <0) {

//显然size不可以小于0

PyErr_SetString(PyExc_SystemError,

"Negative size passed to PyBytes_FromStringAndSize");

return NULL;

}

//如果size为1表名创建的是单字节对象&#xff0c;当然str不可以为NULL, 而且获取到的字节必须要在characters里面

if (size &#61;&#61; 1 && str !&#61; NULL &&

(op &#61; characters[*str & UCHAR_MAX]) !&#61; NULL)

{

#ifdef COUNT_ALLOCS

_Py_one_strings&#43;&#43;;

#endif

//增加引用计数&#xff0c;返回指针

Py_INCREF(op);

return (PyObject *)op;

}

//否则话创建新的PyBytesObject&#xff0c;此时是个空

op &#61; (PyBytesObject *)_PyBytes_FromSize(size, 0);

if (op &#61;&#61; NULL)

return NULL;

if (str &#61;&#61; NULL)

return (PyObject *) op;

//不管size是对少&#xff0c;都直接拷贝即可

memcpy(op->ob_sval, str, size);

//但是size是1的话&#xff0c;除了拷贝还会放到缓存池characters中

if (size &#61;&#61; 1) {

characters[*str & UCHAR_MAX] &#61; op;

Py_INCREF(op);

}

//返回其指针

return (PyObject *) op;

}

由此可见&#xff0c;当 Python 程序开始运行时&#xff0c;字符缓冲池是空的。随着单字节 bytes*对象的创建&#xff0c;缓冲池中的对象慢慢多了起来。

这样一来&#xff0c;字符对象首次创建后便在缓冲池中缓存起来&#xff1b;后续再次使用时&#xff0c; Python 直接从缓冲池中取&#xff0c;避免重复创建和销毁。与前面章节介绍的小整数对象池一样&#xff0c;字符对象只有为数不多的 256 个&#xff0c;但使用频率非常高。缓冲池技术作为一种以时间换空间的优化手段&#xff0c;只需较小的内存为代价&#xff0c;便可明显提升执行效率。

>>> a1 &#61; b"a"

>>> a2 &#61; b"a"

>>> a1 is a2

True

>>>

>>> a1 &#61; b"ab"

>>> a2 &#61; b"ab"

>>> a1 is a2

False

>>>

显然此时不需要我解释了&#xff0c;单字节bytes对象会缓存起来&#xff0c;不是单字节则不会缓存。

bytearray对象

除了bytes对象之外&#xff0c;Python中还有一个bytearray对象&#xff0c;它和bytes对象类似&#xff0c;只不过bytes对象是不可变的&#xff0c;而bytearray对象是可变的。所以就不单独分析了&#xff0c;这里简单提一嘴。

# 传入一个整型组成的列表创建bytearray对象

s &#61; bytearray([99, 100, 101])

print(s) # bytearray(b&#39;cde&#39;)

# 传入一个bytes对象创建bytearray对象

s &#61; bytearray(b"abc")

print(s)

# 传入一个字符串&#xff0c;同时指定encoding编码创建bytearray对象

s &#61; bytearray("古明地觉", encoding&#61;"utf-8")

print(s) # bytearray(b&#39;\xe5\x8f\xa4\xe6\x98\x8e\xe5\x9c\xb0\xe8\xa7\x89&#39;)

# 我们对s进行decode会直接得到字符串

print(s.decode("utf-8")) # 古明地觉

# 注意&#xff1a;bytearray对象是可以变的

# 如果是中文&#xff0c;为了防止出现乱码&#xff0c;所以一次要改变3个字节

s[-3:] &#61; "恋".encode("utf-8")

print(s) # bytearray(b&#39;\xe5\x8f\xa4\xe6\x98\x8e\xe5\x9c\xb0\xe6\x81\x8b&#39;)

print(s.decode("utf-8")) # 古明地恋

# 我们同样可以根据索引、切片获取

s &#61; bytearray(b"abc")

# 获取单个元素也会得到整型&#xff0c;这一点和bytes对象是一样的

print(s[0], s[1], s[2]) # 97 98 99

# 通过切片得到bytearray

print(s[:2]) # bytearray(b&#39;ab&#39;)

# 对多个bytearray对象进行join, 会得到一个bytes对象

print(b"--".join([bytearray(b"abc"), bytearray(b"def")])) # b&#39;abc--def&#39;

因此把bytearray对象想象成可变的bytes对象即可&#xff0c;它的使用和bytes对象非常类似&#xff0c;一些操作的行为也是一样的&#xff0c;所以就不单独分析了&#xff0c;下一篇将会分析Python中的字符串。

小结

这次我们分析了bytes对象的底层实现&#xff0c;我们说&#xff1a;

bytes对象是一个变长、不可变对象&#xff0c;内部的值是通过一个C的字符数组来维护的;

bytes也是序列型操作&#xff0c;它支持的操作在bytes_as_sequence中;

Python内部维护字符缓冲池来优化单字节bytes对象的创建和销毁操作;

缓冲池是一种常用的以空间换时间的优化技术;



推荐阅读
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • C# 7.0 新特性:基于Tuple的“多”返回值方法
    本文介绍了C# 7.0中基于Tuple的“多”返回值方法的使用。通过对C# 6.0及更早版本的做法进行回顾,提出了问题:如何使一个方法可返回多个返回值。然后详细介绍了C# 7.0中使用Tuple的写法,并给出了示例代码。最后,总结了该新特性的优点。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • 本文介绍了为什么要使用多进程处理TCP服务端,多进程的好处包括可靠性高和处理大量数据时速度快。然而,多进程不能共享进程空间,因此有一些变量不能共享。文章还提供了使用多进程实现TCP服务端的代码,并对代码进行了详细注释。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 从零学Java(10)之方法详解,喷打野你真的没我6!
    本文介绍了从零学Java系列中的第10篇文章,详解了Java中的方法。同时讨论了打野过程中喷打野的影响,以及金色打野刀对经济的增加和线上队友经济的影响。指出喷打野会导致线上经济的消减和影响队伍的团结。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • 本文介绍了在mac环境下使用nginx配置nodejs代理服务器的步骤,包括安装nginx、创建目录和文件、配置代理的域名和日志记录等。 ... [详细]
author-avatar
枫涵笑
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有