python中bytes用法,python from_bytes

  python中bytes用法,python from_bytes

  本文主要介绍对Python内置类型bytes的深入理解。有需要的朋友可以借鉴一下,希望能有所帮助。祝大家进步很大,早日升职加薪。

  00-1010简介1 bytes和str的关系2bytes对象的结构:PyBytesObject的行为3 bytes对象3.1 pybytes _ type 3.2 bytes _ as _ sequence 4字符缓冲池

  

目录

  “深度理解Python内置类型”的内容将从源码的角度向大家介绍Python中各种常用的内置类型。

  在我们的日常开发中,str是非常常用的内置类型,bytes是我们很少接触的一种。这里我先为大家介绍一下bytes的相关知识点,下一篇博客再详细介绍str的相关内容。

  

引言

  很多语言字符串都是用字符数组(或字节序列)来表示的,比如C语言:

  char str[]=Hello World!;

  由于一个字节最多只能表示256个字符,如果要覆盖很多字符(比如汉字),就需要用多个字节来表示一个字符,也就是多字节编码。但由于原始字节序列中没有维护编码信息,操作不慎很容易导致各种乱码。

  Python提供的解决方案是使用Unicode对象(也就是str对象)。Unicode口语表示各种字符,所以不需要关心编码。但是,在存储或网络通信过程中,字符串对象需要序列化为字节序列。因此,Python提供了一个额外的——字节的字节序列对象。

  strbytes和strbytes之间的关系如图所示:

  Str对象统一表示一个字符串,不需要关心编码;计算机通过字节序列处理存储媒体和网络媒体,字节序列用bytes对象表示;在存储或传输str对象时,需要将其序列化为字节序列,序列化的过程也是编码的过程。

  

1 bytes和str之间的关系

  c源代码:

  typedef结构{

  PyObject_VAR_HEAD

  Py _ hash _ t ob _ shash

  char ob _ sval[1];

  /*不变量:

  * ob_sval包含用于“ob_size 1”元素的空间。

  * ob_sval[ob_size]==0。

  * ob_shash是字符串的散列,如果尚未计算,则为-1。

  */

  } PyBytesObject

  源代码分析:

  字符ob_sval存储对应的字符,但是ob_sval数组的长度不是ob_size,而是ob_size 1。这是Python为要存储的字节序列多分配了一个字节,用来保存末尾的 \0 ,以兼容C字符串。

  Ob_shash:用于保存字节序列的哈希值。因为计算bytes对象的哈希值需要遍历其内部的字符数组,开销比较大。所以Python选择保存哈希值,用空间换时间(无处不在的思想,hh),避免重复计算。

  图示如下:

  

2 bytes对象的结构:PyBytesObject

  

3 bytes对象的行为

  c源代码:

  PyTypeObject PyBytes_Type={

  PyVarObject _ HEAD _ INIT(PyType _ Type,0)

  字节,

  PyBytesObject_SIZE,

  sizeof(char),

  //.

  bytes_as_number,/* tp_as_number */

  字节序列

  nce, /* tp_as_sequence */

   &bytes_as_mapping, /* tp_as_mapping */

   (hashfunc)bytes_hash, /* tp_hash */

   // ...

  };

  

  数值型操作bytes_as_number:

  

static PyNumberMethods bytes_as_number = {

   0, /*nb_add*/

   0, /*nb_subtract*/

   0, /*nb_multiply*/

   bytes_mod, /*nb_remainder*/

  };

  

  bytes_mod:

  

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对象只是借用%运算符实现字符串格式化,并不是真正意义上的数值运算(这里其实和最开始的分类标准是有点歧义的,按标准应该再分一个格式型操作,不过灵活处理也是必须的):

  

>>> bmsg: a = %d, b = %d % (1, 2)

  bmsg: a = 1, b = 2

  

  序列型操作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个:

  

  • bytes_length:查询序列长度
  • bytes_concat:将两个序列合并为一个
  • bytes_repeat:将序列重复多次
  • bytes_item:取出给定下标的序列元素
  • bytes_contains:包含关系判断

  关联型操作bytes_as_mapping:

  

static PyMappingMethods bytes_as_mapping = {

   (lenfunc)bytes_length,

   (binaryfunc)bytes_subscript,

   0,

  };

  

  可以看到bytes支持获取长度和切片两个操作。

  

  

3.2 bytes_as_sequence

  这里我们主要介绍以下bytes_as_sequence相关的操作

  bytes_as_sequence中的操作都不复杂,但是会有一个陷阱,这里我们以bytes_concat操作来认识一下这个问题。C源码如下:

  

/* This is also used by PyBytes_Concat() */

  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, "cant concat %.100s to %.100s",

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

   goto done;

   }

   /* Optimize end cases */

   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;

  }

  

  bytes_concat源码大家可自行分析,这里直接以图示形式来展示,主要是为了说明其中的陷阱。

  图示如下:

  

  

  • Py_buffer提供了一套操作对象缓冲区的统一接口,屏蔽不同类型对象的内部差异
  • bytes_concat则将两个对象的缓冲区拷贝到一起,形成新的bytes对象

  上述的拷贝过程是比较清晰的,但是这里隐藏着一个问题——数据拷贝的陷阱。

  以合并3个bytes对象为例:

  

>>> a = babc

  >>> b = bdef

  >>> c = bghi

  >>> result = a + b + c

  >>> result

  babcdefghi

  

  本质上这个过程会合并两次

  

>>> t = a + b

  >>> result = t + c

  

  在这个过程中,a和b的数据都会被拷贝两遍,图示如下:

  

  不难推出,合并n个bytes对象,头两个对象需要拷贝n - 1次,只有最后一个对象不需要重复拷贝,平均下来每个对象大约要拷贝n/2次。因此,下面的代码:

  

>>> result = b

  >>> for b in segments:

   result += s

  

  效率是很低的。我们可以使用join()来优化:

  

>>> result = b.join(segments)

  join()方法是bytes对象提供的一个内建方法,可以高效合并多个bytes对象。join方法对数据拷贝进行了优化:先遍历待合并对象,计算总长度;然后根据总长度创建目标对象;最后再遍历待合并对象,逐一拷贝数据。这样一来,每个对象只需要拷贝一次,解决了重复拷贝的陷阱。(具体源码大家可以自行去查看)

  

  

4 字符缓冲池

  和小整数一样,字符对象(即单字节的bytes对象)数量也很少,只有256个,但使用频率非常高,因此以空间换时间能明显提升执行效率。字符缓冲池源码如下:

  

static PyBytesObject *characters[UCHAR_MAX + 1];

  下面我们从创建bytes对象的过程来看一下字符缓冲池的使用:PyBytes_FromStringAndSize()函数是负责创建bytes对象的通用接口,源码如下:

  

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

   /* share short strings */

   if (size == 1) {

   characters[*str & UCHAR_MAX] = op;

   Py_INCREF(op);

   }

   return (PyObject *) op;

  }

  

  其中涉及字符缓冲区维护的关键步骤如下:

  第10~17行:如果创建的对象为单字节对象,会先在characters数组的对应序号判断是否已经有相应的对象存储在了缓冲区中,如果有则直接取出

  第28~31行:如果创建的对象为单字节对象,并且之前已经判断了不在缓冲区中,则将其放入字符缓冲池的对应位置

  由此可见,当Python程序开始运行时,字符缓冲池是空的。随着单字节bytes对象的创建,缓冲池中的对象就慢慢多了起来。当缓冲池已缓存b’1’、b’2’、b’3’、b’a’、b’b’、b’c’这几个字符时,内部结构如下:

  

  示例:

  注:这里大家可能在IDLE和PyCharm中获得的结果不一致,这个问题在之前的博客中也提到过,查阅资料后得到的结论是:IDLE运行和PyCharm运行的方式不同。这里我将PyCharm代码对应的代码对象反编译的结果展示给大家,但我对IDLE的认识还比较薄弱,以后有机会再给大家详细补充这个知识(抱拳~)。

  这里大家还是先以认识字符缓冲区这个概念为主,当然字节码的相关知识掌握好了也是很有帮助的。以下是PyCharm运行的结果:

  以下操作的相关讲解可以看这篇博客:Python程序执行过程与字节码

  示例1:

  

  

  下面我们来看一下反编译的结果:(下面的文件路径我省略了,大家自己试验的时候要输入正确的路径)

  

>>> text = open(D:\\...\\test2.py).read()

  >>> result= compile(text,D:\\...\\test2.py, exec)

  >>> import dis

  >>> dis.dis(result)

   1 0 LOAD_CONST 0 (ba)

   2 STORE_NAME 0 (a)

   2 4 LOAD_CONST 0 (ba)

   6 STORE_NAME 1 (b)

   3 8 LOAD_NAME 2 (print)

   10 LOAD_NAME 0 (a)

   12 LOAD_NAME 1 (b)

   14 IS_OP 0

   16 CALL_FUNCTION 1

   18 POP_TOP

   20 LOAD_CONST 1 (None)

   22 RETURN_VALUE

  

  可以很清晰地看到,第5行和第8行的LOAD_CONST指令操作的都是下标为0的常量b’a’,因此此时a和b对应的是同一个对象,我们打印看一下:

  

>>> result.co_consts[0]

  ba

  

  示例2:

  为了确认只会缓存单字节的bytes对象,我在这里又尝试了多字节的bytes对象,同样还是在PyCharm环境下尝试:

  

  

  结果是比较出乎意料的:多字节的bytes对象依然是同一个。为了验证这个想法,我们先来看一下对代码对象的反编译结果:

  

>>> text = open(D:\\...\\test3.py).read()

  >>> result= compile(text,D:\\...\\test3.py, exec)

  >>> import dis

  >>> dis.dis(result)

   1 0 LOAD_CONST 0 (babc)

   2 STORE_NAME 0 (a)

   2 4 LOAD_CONST 0 (babc)

   6 STORE_NAME 1 (b)

   3 8 LOAD_NAME 2 (print)

   10 LOAD_NAME 0 (a)

   12 LOAD_NAME 1 (b)

   14 IS_OP 0

   16 CALL_FUNCTION 1

   18 POP_TOP

   20 LOAD_CONST 1 (None)

   22 RETURN_VALUE

  >>> result.co_consts[0]

  babc

  

  可以看到,反编译的结果和单字节的bytes对象没有区别。。。

  (TODO:这里我尝试去看了PyBytes_FromStringAndSize()中相关的其他调用,但是由于水平有限,没有找到这个问题的解释,这个问题先暂时放下,随着理解源码更深刻再继续解决)

  以上就是Python内建类型bytes深入理解的详细内容,更多关于Python内建类型bytes的资料请关注盛行IT软件开发工作室其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: