万物之中, 希望至美.

「Python3学习笔记」读书笔记—字节数组

2018.06.21

生物都是由细胞构成的,但在我们普通人眼中,并不会将鸡、鸭、狗、鸟这些动物当作细胞看待,因为对待事物的角度决定了我们更关心生物的外在形状和行为,而不是它的组织构成。

从计算机底层实现来说,所有的数据都是二进制字节序列。但为了更好地表达某个逻辑,计算机科学家们将数据抽象成不同的类型,犹如细胞和动物的关系。在编程语言中,对于字节序列,我们更关心的是它的存储和传输方式;而面向对象时,则着重于它的抽象属性。尽管两面一体,但从不混为一谈。

同为不可变序列类型,bytes 与 str 有着非常相似的操作方式。其同样支持加法、乘法等运算符。

>>> a = b"abc"
>>> b = a + b"def"

>>> b
b'abcdef'

>>> b.startswith(b"a")
True
>>> b.upper()
b'ABCDEF'

>>> b"abc" * 2
b'abcabc'

相比于 bytes 类型的一次性分配内存,bytearry 可按需扩张,更适合作为可读写缓冲区使用。如有必要,还可为其提前分配足够的内存,避免中途扩张造成的额外消耗。

>>> b = bytearray(b"abc")
>>> len(b)
3
>>> id(b)
4473445824
>>> b.append(ord("d"))
>>> b.extend(b"e")
>>> id(b)
4473445824

内存视图

当我们要引用字节数据的某个片段的时候,需要考虑到:是否会有数据复制行为?是否能同步修改?

>>> a = bytearray([0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16])
>>> x = a[2:5]	# 引用片段
>>> x
bytearray(b'\x12\x13\x14')
>>> a[3] = 0xEE	# 修改原数据
>>> a
bytearray(b'\x10\x11\x12\xee\x14\x15\x16')
>>> x		# 并未同步修改,可以看出仅仅只是数据复制
bytearray(b'\x12\x13\x14')

为什么需要引用某个片段,而不是整个对象呢?

以自定义网络协议为例,通常由标准头和数据体两部分组成。如要验证数据是否被修改,总不能将整个包作为参数交给验证函数吧。因为如果将整个包传给函数,这势必要求该函数了解协议包的结构,这显然是不合理的设计,而复制数据体又可能导致重大性能开销,同样得不偿失。

在 Python 中没有指针的概念,外加内存安全模型的限制,要做到这一点并不容易。此时,可以借助一种名为**内存视图(Memory View)**的方式来访问底层内存数据。

内存视图要求目标对象支持缓冲协议(Buffer Protocol),内存视图直接引用目标内存,没有额外的复制行为,因此,可读取最新的数据,在目标对象允许的情况下,还可以执行写操作。

>>> a = bytearray([0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16])
>>> v = memoryview(a)		# 完整的视图
>>> x = v[2:5]		# 视图片段
>>> x.hex()
'121314'

>>> a[3] = 0xee 	# 对原数据进行修改,可通过视图观察到
>>> a
bytearray(b'\x10\x11\x12\xee\x14\x15\x16')
>>> x.hex()
'12ee14'

>>> x[1] = 0x13		# 因为引用了相同的内存区域,可通过视图修改原数据
>>> a
bytearray(b'\x10\x11\x12\x13\x14\x15\x16')

当然,能够通过视图修改原数据,还必须得看原对象是否允许。

>>> a = b"\x10\x11"		# bytes 是不可变类型
>>> v = memoryview(a)
>>> v[1] = 0xEE
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot modify read-only memory

如果要复制视图数据,可调用 tobytes、tolist 方法,复制后的数据与原对象无关,同样不会影响视图本身。

>>> a = bytearray([0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16])
>>> v = memoryview(a)
>>> x = v[2:5]
>>> b = x.tobytes()		# 复制并返回视图数据
>>> b
b'\x12\x13\x14'
>>> a[3] = 0xEE		# 对原数据进行修改
>>> a
bytearray(b'\x10\x11\x12\xee\x14\x15\x16')
>>> b		# 不影响复制数据
b'\x12\x13\x14'

除了上述这些,内存视图还为我们提供了一种内存管理手段,比如:通过 bytearray 预申请很大的一块内存,随后以视图方式将不同片段交给不同的逻辑使用。因为逻辑不能跨界访问,故此可以实现简易的内存分配器模式。对于 Python 这种限制较多的语言,合理使用视图可在不同使用 ctypes 等复制扩展的前提下,改善算法类型。

可使用 memoryview.cast、struct.unpack 将字节数组转换为目标类型。

comments powered by Disqus