生物都是由细胞构成的,但在我们普通人眼中,并不会将鸡、鸭、狗、鸟这些动物当作细胞看待,因为对待事物的角度决定了我们更关心生物的外在形状和行为,而不是它的组织构成。
从计算机底层实现来说,所有的数据都是二进制字节序列。但为了更好地表达某个逻辑,计算机科学家们将数据抽象成不同的类型,犹如细胞和动物的关系。在编程语言中,对于字节序列,我们更关心的是它的存储和传输方式;而面向对象时,则着重于它的抽象属性。尽管两面一体,但从不混为一谈。
同为不可变序列类型,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 "", line 1, in
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 将字节数组转换为目标类型。
从博客搬运到:原文链接