Python内建类型bytes深入理解

目录
  • 引言
  • 1 bytes和str之间的关系
  • 2 bytes对象的结构:PyBytesObject
  • 3 bytes对象的行为
    • 3.1 PyBytes_Type
    • 3.2 bytes_as_sequence
  • 4 字符缓冲池

引言

“深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型。

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

1 bytes和str之间的关系

不少语言中的字符串都是由字符数组(或称为字节序列)来表示的,例如C语言:

char str[] = "Hello World!";

由于一个字节最多只能表示256种字符,要想覆盖众多的字符(例如汉字),就需要通过多个字节来表示一个字符,即多字节编码。但由于原始字节序列中没有维护编码信息,操作不慎就很容易导致各种乱码现象。

Python提供的解决方法是使用Unicode对象(也就是str对象),Unicode口语表示各种字符,无需关心编码。但是在存储或者网络通讯时,字符串对象需要序列化成字节序列。为此,Python额外提供了字节序列对象——bytes。

str和bytes的关系如图所示:

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

2 bytes对象的结构:PyBytesObject

C源码:

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;

源码分析:

字符数组ob_sval存储对应的字符,但是ob_sval数组的长度并不是ob_size,而是ob_size + 1.这是Python为待存储的字节序列额外分配了一个字节,用于在末尾处保存’\0’,以便兼容C字符串。

ob_shash:用于保存字节序列的哈希值。由于计算bytes对象的哈希值需要遍历其内部的字符数组,开销相对较大。因此Python选择将哈希值保存起来,以空间换时间(随处可见的思想,hh),避免重复计算。

图示如下:

3 bytes对象的行为

3.1 PyBytes_Type

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 */
    // ...
};

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

>>> b'msg: a = %d, b = %d' % (1, 2)
b'msg: 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, "can't 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 = b'abc'
>>> b = b'def'
>>> c = b'ghi'
>>> result = a + b + c
>>> result
b'abcdefghi'

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

>>> 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源码学习笔记: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 (b'a')
              2 STORE_NAME               0 (a)
  2           4 LOAD_CONST               0 (b'a')
              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]
b'a'

示例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 (b'abc')
              2 STORE_NAME               0 (a)
  2           4 LOAD_CONST               0 (b'abc')
              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]
b'abc'

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

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

以上就是Python内建类型bytes深入理解的详细内容,更多关于Python内建类型bytes的资料请关注我们其它相关文章!

(0)

相关推荐

  • python中bytes和str类型的区别

    经过一上午的查找资料.大概理清楚了bytes类型和str类型的区别. bytes类型和str类型在呈现形式有相同之处,如果你print一个bytes类型的变量,会打印一个用b开头,用单引号括起来的序列.比如: >>> c = b'\x80abc' >>> type(c) bytes 我们看到c = b'\x80abc'表示的就是一个bytes类型.是不是和字符串很像?只是前面多出来一个b.那b'\x80abc的含义是什么呢?\x80即16进制的两位数,代表十进制的0-2

  • Python中的bytes类型用法及实例分享

    目录 1.bytes定义 2.bytes方法 3.使用不同方式创建bytes对象 前言; Python bytes 类型用来表示一个字节串.“字节串“不是编程术语,是我自己“捏造”的一个词,用来和字符串相呼应.bytes 是 Python 3.x 新增的类型,在 Python 2.x 中是不存在的. 字节串(bytes)和字符串(string)的对比: 字符串由若干个字符组成,以字符为单位进行操作:字节串由若干个字节组成,以字节为单位进行操作. 字节串和字符串除了操作的数据单元不同之外,它们支持

  • python3 字符串str和bytes相互转换

    目录 1.相关基础 2.str和bytes相互转换 2.1 bytes->str 2.2 str->bytes 1.相关基础 python3中有两种字符串类型:str和bytes python编码问题可以参考文章 str以unicode编码格式保存在内存 所以使用时,不用管前面要不要加u (python2中需要考虑,不加u的话,在一些场合会报错) #!/usr/bin/python3 str0="i am fine thank you"   print(type(str0)

  • Python bytes string相互转换过程解析

    一.bytes和string区别 1.python bytes 也称字节序列,并非字符.取值范围 0 <= bytes <= 255,输出的时候最前面会有字符b修饰:string 是python中字符串类型; 2.bytes主要是给在计算机看的,string主要是给人看的: 3.string经过编码encode,转化成二进制对象,给计算机识别:bytes经过解码decode,转化成string,让我们看,但是注意反编码的编码规则是有范围,\xc8就不是utf8识别的范围: if __name_

  • Python中bytes字节串和string字符串之间的转换方法

    目录 背景 代码 代码说明: 验证一下 附:bytes和string区别 总结 背景 在工作中经常会碰到字节串(bytes)与字符串(string)之间转换的问题,做个记录. bytes只负责用字节序列的形式(二进制形式)存储数据,不关心数据本身是图片.文字.视频等等.如果需要使用并且展示的话,按照对应的解析规则处理,就可以拿到对应类型的数据.如常见的字符串类型,只需要使用对应的字符编码格式,就可以拿到字符串的内容. 下面会用一些代码来表示bytes的构造,以及和字符串之间的转换. 代码 先看一

  • python数据类型bytes 和 bytearray的使用与区别

    目录 bytes 和 bytearray 理解bytes 和 bytearray python中值的表示 进制转换 bytes类型 bytes类型转化 bytes转数值 bytes的方法 bytearray bytearray的方法 bytes 和 bytearray bytes 和 bytearray 都是二进制世界的成员,用二进制的方式去理解才能看清他的本质. 理解bytes 和 bytearray 0 和 1 是计算机工作的根本,单个的0和1只能表达两种状态,无法满足我们复杂的计算,于是计

  • Python内建类型bytes深入理解

    目录 引言 1 bytes和str之间的关系 2 bytes对象的结构:PyBytesObject 3 bytes对象的行为 3.1 PyBytes_Type 3.2 bytes_as_sequence 4 字符缓冲池 引言 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 在我们日常的开发中,str是很常用的一个内建类型,与之相关的我们比较少接触的就是bytes,这里先为大家介绍一下bytes相关的知识点,下一篇博客再详细介绍str的相关内容

  • Python内建类型str源码学习

    目录 引言 1 Unicode 2 Python中的Unicode 2.1 Unicode对象的好处 2.2 Python对Unicode的优化 3 Unicode对象的底层结构体 3.1 PyASCIIObject 3.2 PyCompactUnicodeObject 3.3 PyUnicodeObject 3.4 示例 4 interned机制 5 总结 引言 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 在介绍常用类型str之前,在上

  • Python内建类型int源码学习

    目录 1 int对象的设计 1.1 PyLongObject 1.2 整数的布局 1.3 小整数静态对象池 1.4 示例 2 大整数运算 2.1 整数运算概述 2.2 大整数运算处理过程 1.long_add()源码: 2.绝对值加法x_add() 3 其他 大整数转float溢出 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 问题:对于C语言,下面这个程序运行后的结果是什么?是1000000000000吗? #include <stdio

  • python内建类型与标准类型

    目录 前言 理解对象和类型 关于不可变类型和可变类型 关于动态静态强弱类型 标准类型 其它内建类型 类型的类型 None ->空类型 内建类型的布尔值 前言 全可以访问相同的对象, 因此我们讲 这种变量名也叫对象的 "引用". 验证1: a = 2 b = 3 print(id(a),id(b))  #140734889681584 140734889681616 b = 2 print(id(b))    #140734889681584 验证2: b = 3 print(id

  • Python内建类型float源码学习

    目录 1 回顾float的基础知识 1.1 PyFloatObject 1.2 PyFloat_Type 1.3 对象的创建 1.4 对象的销毁 1.5 小结 2 空闲对象缓存池 2.1 浮点对象的空闲链表 2.2 空闲链表的使用 3 其他 “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种常用的内建类型. 1 回顾float的基础知识 1.1 PyFloatObject 1.2 PyFloat_Type C源码(仅列出部分字段): PyTypeObject P

  • Python内建类型list源码学习

    目录 问题: 1 常用方法 小结: 题外话: 2 list的内部结构:PyListObject 3 尾部操作和头部操作 3.1 尾部操作 3.2 头部操作 4 浅拷贝和深拷贝 4.1 浅拷贝 4.2 深拷贝 4.3 直接赋值 4.4 小结 个人总结: TODO: 5 动态数组 5.1 容量调整 5.2 append() 5.3 insert() 5.4 pop() 5.5 remove() 6 一些问题 问题: “深入认识Python内建类型”这部分的内容会从源码角度为大家介绍Python中各种

  • python基础入门详解(文件输入/输出 内建类型 字典操作使用方法)

    一.变量和表达式 复制代码 代码如下: >>> 1 + 1               2>>> print 'hello world' hello world>>> x = 1               >>> y = 2>>> x + y3 Python是强类型语言,无法根据上下文自动解析转换成合适的类型. Python是一种动态语言,在程序运行过程中,同一个变量名在运行的不同阶段可以代表不同形式的值(整型,浮

  • Python内建模块struct实例详解

    本文研究的主要是Python内建模块struct的相关内容,具体如下. Python中变量的类型只有列表.元祖.字典.集合等高级抽象类型,并没有像c中定义了位.字节.整型等底层初级类型.因为Python本来就是高级解释性语言,运行的时候都是经过翻译后再在底层运行.如何打通Python和其他语言之间的类型定义障碍,Python的内建模块struct完全解决了所有问题. 知识介绍: 在struct模块中最最常用的三个: (1)struct.pack:用于将Python的值根据格式符,转换为字符串(因

  • Python内置数据类型详解

    通常来说Python在编程语言中的定位为脚本语言--scripting language 高阶动态编程语言. Python是以数据为主,变量的值改变是指变量去指到一个地址. 即:Id(变量)->展示变量的地址. 因此一个具体的值,会有不同的变量名. Python的数据类型: 数字.字符串.列表.元组.字典 数字和字符串其实是很基本的数据类型,在Python中和其他语言相差不是很大的,在这里就不细讲了. Dictionary介绍: Dictionary是Python的内置数据类型之一,它定义了键和

  • python内置数据类型之列表操作

    数据类型是一种值的集合以及定义在这种值上的一组操作.一切语言的基础都是数据结构,所以打好基础对于后面的学习会有百利而无一害的作用. python内置的常用数据类型有:数字.字符串.Bytes.列表.元组.字典.集合.布尔等 1.什么是列表 lst[#] 通过下标访问,从0开始. ps:如果#超过下标的范围时候会出现IndexError的错误. 如果#为负号,则索引从右边开始,#无论为正负均有范围,超过范围会报错. lst = list(range(0,9)) #生产列表 l1 = lst[3]

随机推荐