打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
《源码探秘 CPython》15. bytes 对象是怎么实现的?

楔子

我们知道在C语言里面,有一个概念叫字符数组,比如:

char name[] = "komeiji satori";

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

而Python提供的解决方案是使用unicode(在Python3中等价于str)来表示字符串,因为unicode可以表示各种字符,不需要关心编码的问题,我们后面会详细解析字符串。

但在存储或网络通讯时,传输的都是二进制,字符串不可避免地要序列化成字节序列。为此,Python除了提供字符串之外,还额外提供了字节序列(字节串),也就是 bytes 对象。

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

既然有序列化,那么就有反序列化,很明显反序列化是将bytes对象转成str对象,也被称为解码

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

bytes 对象的底层结构

我们说bytes对象是一个字节序列、或者字节串,那么显然它是由若干个字节组成的,也就意味着它是一个变长对象。字节序列内部的字节数,就是其长度。

//Include/bytesobject.htypedef struct {    PyObject_VAR_HEAD    Py_hash_t ob_shash;    char ob_sval[1];} 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类型的数组,一个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"

显然长度等于32+4=36字节。

所以 bytes 对象的底层结构还是很好理解的,因为它是字节序列,所以在底层用一个char类型的数组来维护具体的值再合适不过了。

创建 bytes 对象

下面我们来看一下 bytes 对象的创建方式,这里我们暂时先不介绍底层是如何创建的,等到介绍缓存池的时候再说。我们来聊一聊如何在Python中创建,虽然我们这里是探秘CPython,但是光说底层的话可能会有一些无趣,因此这个过程中也会穿插大量的Python内容。

b1 = b"hello"

以上是最简单的创建方式了,采用我们之前说的Python/C API创建,但这种创建方式只使用于ASCII字符。下面这种方式是不行的:

b = b"古明地觉"

"古明地觉"包含非ASCII字符,所以采用多字节编码(关于字符编码、字符集等概念在介绍字符串的时候会详细说),但编码方式也有多种,比如utf-8、gbk等等,Python不知道你用的是哪一种。因此采用字面量的方式,只能使用ASCII字符串,如果使用非ASCII字符串,那么必须手动指定编码。

b = bytes("古明地觉", encoding="utf-8")print(b)  # b'\xe5\x8f\xa4\xe6\x98\x8e\xe5\x9c\xb0\xe8\xa7\x89'

里面的 \x 表示16进制,我们知道字符a的ASCII码是97,对应16进制是61,同理字符b是62,字符c是63,那么 b"abc" 就还可以这么创建:

b = b"\x61\x62\x63"print(b)  # b'abc'

以上是根据16进制的数字创建bytes对象,注意:采用字面量的方式创建必须指定\x,b"\x61"表示的是1个字节,并且该字节对应的ASCII码的16进制等于61,也就是字符a;而b"61"表示的是两个字节。

# \x61、\x62、\x63均表示1字节print(b"\x61\x62\x63")  # b'abc'# 下面这种创建的bytes对象是6字节print(b"616263")  # b'616263'

可如果我有一串字符也是16进制格式,但开头没有\x,这个时候我要怎么转成bytes对象呢?很简单,使用bytes.fromhex方法即可。

print(bytes.fromhex("616263"))  # b'abc'# 转成bytes对象之后,如果能用ASCII字符显示的话# 那么就用ASCII字符显示,比如 abc# 不能的话,就原本输出,比如\xffprint(bytes.fromhex("616263FF"))  # b'abc\xff'

该方法会将里面字符串当成16进制来解析,得到bytes对象。并且使用这种方式的话,字符的个数一定是偶数,每个字符的范围均是0~9、A-F(或者a-f)。因为16进制需要两个字符来表示,范围是00FF。即便小于16,也必须用两个字符表示,比如我们可以写成05,但绝不能只写个5

总之使用bytes.fromhex 创建时,字符串的长度一定是一个偶数,从前往后每两个分为一组。字面量的方式创建时也是如此,比如我们可以写成b"\x01\x02",但绝不能写成b"\x1\x2"

# 不可以写成 b"\x0",会报错b1 = b"\x00"print(b1)  # b'\x00'
# \x后面至少跟两个字符,但这里跟了3个字符# 所以 \x 会和 61 结合,形成 'a'# 至于后面的那个 1 就单纯的表示字符 '1'b2 = b"\x611"print(b2) # b'a1'


所以\x后面可以跟超过两个以上的字符,超过两个以上的部分会当成普通字符来处理,与十六进制无关,每个\x只和它后面两个字符结合;但 \x 后面不能少于两个字符。

问题又来了,如果我有一串整数,是十进制的,这个时候怎么创建呢?

# 里面的每个数值范围均是 0~255b1 = bytes([97, 98, 99])print(b1)  # b'abc'

这种创建方式也是很方便的,总之 bytes 对象的创建方式有多种,相信还是有部分小伙伴没有仔细观察打印bytes对象时输出的内容。核心就在于bytes对象本质上是字节序列,你看到的\x表示的是:该字节是通过\x加上16进制的ASCII码来显示的。

然后我们通过索引获取的时候,得到也是一个整数:

b = "古".encode("utf-8")print(b)  # b'\xe5\x8f\xa4'
lst = [b[0], b[1], b[2]]print(lst) # [229, 143, 164]
print(bytes(lst).decode("utf-8")) # 古

小结

以上就是 bytes 对象底层结构,还是比较简单的,就是用一个 char 类型的数组来存储具体的值。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
python字符串、字符串处理函数及字符串相关操作
类型转换方法库--python
R语言常用函数参考
Java String类
mysql json 函数
Java中String类的concat方法
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服