楔子
本篇文章来聊一聊 bytes 对象,也就是字节串,说到字节串就不得不提字符串。
字符串是由任意数量的字符组成的序列,用于表示文本数据。在大多数编程语言中,字符串被视为一种高层数据类型,能够处理和操作文本信息。
字节串是由任意数量的字节组成的序列,用于表示二进制数据。
字符串具有特定的字符编码,比如 UTF-8、ASCII 等等,这些编码定义了字符如何在内存中表示。而字节串没有预定义的编码,它只是原始的二进制序列,在处理非文本数据时非常有用。
计算机在存储数据以及通过网络传输数据时,数据格式都是二进制字节串,所以如果你想传输一段文本,那么必须先将它转成字节串。
word = "想说的话"
# 字符串无法基于网络传输,它必须要转成字节串,这一过程就叫做序列化
# 序列化时需要指定编码,比如 utf-8,utf-16,gbk 等等
print(word.encode("utf-8"))
"""
b'\xe6\x83\xb3\xe8\xaf\xb4\xe7\x9a\x84\xe8\xaf\x9d'
"""
print(word.encode("utf-16"))
"""
b'\xff\xfe\xf3`\xf4\x8b\x84v\xdd\x8b'
"""
print(word.encode("gbk"))
"""
b'\xcf\xeb\xcb\xb5\xb5\xc4\xbb\xb0'
"""
字节串传输之后还需要转成文本数据,这个过程叫做反序列化。但由于字节串不保存编码信息,它只是一坨字节流,因此反序列化时还需要知道指定的编码,如果编码指定错误,那么反序列化会失败。
# word_utf8 和 word_gbk 都只是普通的字节串
word_utf8 = b'\xe6\x83\xb3\xe8\xaf\xb4\xe7\x9a\x84\xe8\xaf\x9d'
word_gbk = b'\xcf\xeb\xcb\xb5\xb5\xc4\xbb\xb0'
# 如果反序列化,必须要知道原始文本使用的编码是什么
print(word_utf8.decode("utf-8"))
print(word_gbk.decode("gbk"))
"""
想说的话
想说的话
"""
# 如果指定了错误的编码,那么反序列化会失败
try:
word_utf8.decode("gbk")
except UnicodeDecodeError as e:
print(e)
"""
'gbk' codec can't decode byte 0xaf in position 4: illegal multibyte sequence
"""
另外我们看到字符串只有 4 个字符,但序列化之后的字节串却明显多于 4 个字节。这是因为一个字节最多能表示 256 个字符,对于英文字符来说已经足够了,但对于非英文字符则力不从心,毕竟光普通的中文字符就好几千个。
所以便有了多字节编码,它使用多个字节来表示一个字符,具体使用多少个,则取决于编码。如果是 GBK 编码,那么两个字节表示一个字符,如果是 UTF-8 编码,那么三个字节表示一个字符。
所以在反序列化的时候,需要指定正确的编码,否则解析一定会失败。
以上就是关于 bytes 对象的一些基础概念,下面来看一下它的底层结构。
字节串的底层结构
字节串的类型是 bytes,那么我们有理由相信它在底层由 PyBytesObject 结构体表示。
// Include/cpython/bytesobject.h
typedef struct {
PyObject_VAR_HEAD
Py_hash_t ob_shash;
char ob_sval[1];
} PyBytesObject;
我们看一下里面的成员对象:
PyObject_VAR_HEAD:变长对象的公共头部,因为字节串是由若干个字节组成的,具有长度的概念,所以它是变长对象。
ob_shash:保存字节串的哈希值,因为计算哈希值需要遍历所有的字节,如果每获取一次哈希值都要重新计算的话,性能会有影响。所以第一次计算之后会用 ob_shash 字段保存起来,之后就不再计算了。如果 bytes 对象的哈希值尚未计算,那么 ob_shash 为 -1。
ob_sval:char 类型的数组,负责保存具体的字节。这个和整数的 ob_digit 字段的声明方式类似,由于数组长度不属于类型的一部分,因此虽然声明的时候长度是 1,但其实长度不受限制,具体是多少取决于 bytes 对象的字节数量。
我们创建几个不同的 bytes 对象,然后通过画图感受一下: