bytes 对象,不可变的字节序列

老当益壮,宁移白首之心?穷且益坚,不坠青云之志。

—— 唐·王勃·《滕王阁序》

不少编程语言中的 字符串 都是由 字符数组 (或称为 字节序列 )来表示, C 语言就是这样。

1
char msg[] = "Hello, world!";

由于一个字节最多只能表示 256 种字符,用来表示英文字符绰绰有余,想覆盖非英文字符便捉襟见肘了。为了表示众多的非英文字符(比如汉字),计算机先驱们发明了 多字节编码 ——通过多个字节来表示一个字符。由于原始字节序列不维护编码信息,操作不慎便导致各种乱码现象。

Python 提供的解决方案是 Unicode 字符串 ( str )对象, Unicode 可以表示各种字符,无需关心编码。然而存储或者网络通讯时,字符串对象不可避免要 序列化 成字节序列。为此, Python 额外提供了字节序列对象 —— bytes 。

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

好了,我们已经弄明白 str 对象以 bytes 之间的关系,这两者是 Python 中最重要的内建对象之一。 读者对 str 对象应该再熟悉不过了,但对更接近底层的 bytes 对象可能涉猎不多。没关系,经过本节学习,你将彻底掌握它!

对象结构

bytes 对象用于表示由若干字节组成的 字节序列 以及相关的 操作 ,并不关心字节序列的 含义 。因此, bytes 应该是一种 变长对象 ,内部由 C 数组实现。 Include/boolobject.h 头文件中的定义印证了我们的猜测:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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;

字节序列对象 PyBytesObject 中,确实藏着一个字符数组 ob_sval 。注意到 ob_sval 数组长度定义为 1 ,这是 C 语言中定义 变长数组 的技巧。这个技巧在前面章节( int 对象,永不溢出的整数 )中介绍过,这里不再赘述。源码注释表明, Python 为待存储的字节序列额外分配一个字节,用于在末尾处保存 \0 ,以便兼容 C 语言的字符串。

此外,我们还留意到另一个字段 ob_shash ,它用于保存字节序列的 哈希值 。 Python 对象哈希值应用范围很广,比如 dict 字典对象依赖对象哈希值进行存储。由于计算 bytes 对象哈希值需要遍历其内部的字符数组,开销相对较大。因此, Python 选择将哈希值保存起来,以空间换时间,避免重复计算。

最后,以几个典型例子结束 bytes 对象结构介绍,以此加深理解:

由此可见,就算空 bytes 对象( b'' )也是要占用内存空间的,至少变长对象 公共头部 是少不了的。

1
2
>>> sys.getsizeof(b'')
33

bytes 对象占用的内存空间可分为以下个部分进行计算:

  • 变长对象公共头部 24 字节,ob_refcnt 、 ob_type 、 ob_size 每个字段各占用 8 字节;
  • 哈希值 ob_shash 占用 8 字节;
  • 字节序列本身,假设是 n 字节;
  • 额外 1 字节用于存储末尾处的 \0 ;

因此, bytes 对象空间计算公式为 $24+8+n+1$,即 $33+n$,其中 $n$ 为字节序列长度。

学习了 bytes 对象的结构,接下来我们还会探索 bytes 对象的行为以及 python 是如何利用 字节缓冲池 来优化单字节 bytes 对象(也可称为 字符对象 )的创建效率。点击 阅读原文,获取更多细节!

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: