新的二进制序列类型在很多方面与 Python 2 的 str 类型不同。首先要知道,Python 内置了两种基本的二进制序列类型:Python 3 引入的不可变 bytes 类型和 Python 2.6 添加的可变 bytearray 类型。(Python 2.6 也引入了 bytes 类型,但那只不过是 str 类型的别名,与 Python 3 的 bytes 类型不同。)

bytesbytearray 对象的各个元素是介于 0~255(含)之间的整数,而不像 Python 2 的 str 对象那样是单个的字符。然而,二进制序列的切片始终是同一类型的二进制序列,包括长度为 1 的切片,如示例 4-2 所示。

示例 4-2 包含 5 个字节的 bytesbytearray 对象

>>> cafe = bytes('café', encoding='utf_8') ➊
>>> cafe
b'caf\xc3\xa9'
>>> cafe[0] ➋
99
>>> cafe[:1] ➌
b'c'
>>> cafe_arr = bytearray(cafe)
>>> cafe_arr ➍
bytearray(b'caf\xc3\xa9')
>>> cafe_arr[-1:] ➎
bytearray(b'\xa9')

bytes 对象可以从 str 对象使用给定的编码构建。

❷ 各个元素是 range(256) 内的整数。

bytes 对象的切片还是 bytes 对象,即使是只有一个字节的切片。

bytearray 对象没有字面量句法,而是以 bytearray() 和字节序列字面量参数的形式显示。

bytearray 对象的切片还是 bytearray 对象。

 my_bytes[0] 获取的是一个整数,而 my_bytes[:1] 返回的是一个长度为 1 的 bytes 对象——这一点应该不会让人意外。s[0] == s[:1] 只对 str 这个序列类型成立。不过,str 类型的这个行为十分罕见。对其他各个序列类型来说,s[i] 返回一个元素,而 s[i:i+1] 返回一个相同类型的序列,里面是 s[i] 元素。

虽然二进制序列其实是整数序列,但是它们的字面量表示法表明其中有 ASCII 文本。因此,各个字节的值可能会使用下列三种不同的方式显示。

因此,在示例 4-2 中,我们看到的是 b'caf\xc3\xa9':前 3 个字节 b'caf' 在可打印的 ASCII 范围内,后两个字节则不然。

除了格式化方法(formatformat_map)和几个处理 Unicode 数据的方法(包括 casefoldisdecimalisidentifierisnumericisprintableencode)之外,str 类型的其他方法都支持 bytesbytearray 类型。这意味着,我们可以使用熟悉的字符串方法处理二进制序列,如 endswithreplacestriptranslateupper 等,只有少数几个其他方法的参数是 bytes 对象,而不是 str 对象。此外,如果正则表达式编译自二进制序列而不是字符串,re 模块中的正则表达式函数也能处理二进制序列。Python 3.0~3.4 不能使用 % 运算符处理二进制序列,但是根据“PEP 461—Adding % formatting to bytes and bytearray”(https://www.python.org/dev/peps/pep-0461/),Python 3.5 应该会支持。

二进制序列有个类方法是 str 没有的,名为 fromhex,它的作用是解析十六进制数字对(数字对之间的空格是可选的),构建二进制序列:

>>> bytes.fromhex('31 4B CE A9')
b'1K\xce\xa9'

构建 bytesbytearray 实例还可以调用各自的构造方法,传入下述参数。

使用缓冲类对象构建二进制序列是一种低层操作,可能涉及类型转换。示例 4-3 做了演示。

示例 4-3 使用数组中的原始数据初始化 bytes 对象

>>> import array
>>> numbers = array.array('h', [-2, -1, 0, 1, 2]) ➊
>>> octets = bytes(numbers) ➋
>>> octets
b'\xfe\xff\xff\xff\x00\x00\x01\x00\x02\x00' ➌

➊ 指定类型代码 h,创建一个短整数(16 位)数组。

octets 保存组成 numbers 的字节序列的副本。

➌ 这些是表示那 5 个短整数的 10 个字节。

使用缓冲类对象创建 bytesbytearray 对象时,始终复制源对象中的字节序列。与之相反,memoryview 对象允许在二进制数据结构之间共享内存。如果想从二进制序列中提取结构化信息,struct 模块是重要的工具。下一节会使用这个模块处理 bytesmemoryview 对象。

struct 模块提供了一些函数,把打包的字节序列转换成不同类型字段组成的元组,还有一些函数用于执行反向转换,把元组转换成打包的字节序列。struct 模块能处理 bytesbytearraymemoryview 对象。

如 2.9.2 节所述,memoryview 类不是用于创建或存储字节序列的,而是共享内存,让你访问其他二进制序列、打包的数组和缓冲中的数据切片,而无需复制字节序列,例如 Python Imaging Library(PIL)2 就是这样处理图像的。

2Pillow(https://pillow.readthedocs.org/en/latest/)是 PIL 最活跃的派生库。

示例 4-4 展示了如何使用 memoryviewstruct 提取一个 GIF 图像的宽度和高度。

示例 4-4 使用 memoryviewstruct 查看一个 GIF 图像的首部

>>> import struct
>>> fmt = '<3s3sHH'  # ➊
>>> with open('filter.gif', 'rb') as fp:
...     img = memoryview(fp.read())  # ➋
...
>>> header = img[:10]  # ➌
>>> bytes(header)  # ➍
b'GIF89a+\x02\xe6\x00'
>>> struct.unpack(fmt, header)  # ➎
(b'GIF', b'89a', 555, 230)
>>> del header  # ➏
>>> del img

❶ 结构体的格式:< 是小字节序,3s3s 是两个 3 字节序列,HH 是两个 16 位二进制整数。

❷ 使用内存中的文件内容创建一个 memoryview 对象……

❸ ……然后使用它的切片再创建一个 memoryview 对象;这里不会复制字节序列。

❹ 转换成字节序列,这只是为了显示;这里复制了 10 字节。

❺ 拆包 memoryview 对象,得到一个元组,包含类型、版本、宽度和高度。

❻ 删除引用,释放 memoryview 实例所占的内存。

注意,memoryview 对象的切片是一个新 memoryview 对象,而且不会复制字节序列。[ 本书的技术审校之一 Leonardo Rochael 指出,如果使用 mmap 模块把图像打开为内存映射文件,那么会复制少量字节。本书不会讨论 mmap,如果你经常读取和修改二进制文件,可以阅读“mmap—Memory-mapped file support”(https://docs.python.org/3/library/mmap.html)来进一步学习。]

本书不会深入介绍 memoryviewstruct 模块,如果要处理二进制数据,可以阅读它们的文档:“Built-in Types » Memory Views”(https://docs.python.org/3/library/stdtypes.html#memory-views)和“struct—Interpret bytes as packed binary data”(https://docs.python.org/3/library/struct.html)。

简要探讨 Python 的二进制序列类型之后,下面说明如何在它们和字符串之间转换。