Python memoryview:只读视图为什么仍会跟着底层数据改变

10-01 4阅读

交出去的只读数据,为何后来变了

程序从一个可复用缓冲区里切出消息,把只读视图交给后续步骤,以为这份内容已经固定。下一次读取复用了原缓冲区,前一条消息却跟着变化。问题通常出在所有权约定:禁止接收方写入,不代表发送方已经放弃修改同一块内存。

memoryview 能直接访问支持缓冲区协议的对象,切片通常仍然共享底层存储。它适合减少大块数据的重复复制,但共享关系必须随着接口一起说明。需要保留某一时刻的字节内容时,可以明确转换成 bytes,承担一次复制成本。

同时保留视图和快照做对照

下面只使用标准库,保存为 Python 文件即可运行,已在 Python 3.12 验证。四个字节足以展示差异,不需要真实网络或大文件。示例会实际捕获只读写入、缓冲区扩容和释放后读取的错误,并在最后清理所有视图。

Python memoryview:只读视图为什么仍会跟着底层数据改变

AI概念示意图,非真实界面

buffer = bytearray(b'abcd')
view = memoryview(buffer)
readonly = view.toreadonly()
snapshot = bytes(readonly)
buffer[1] = ord('Z')
print('view:', bytes(readonly))
print('snapshot:', snapshot)
assert bytes(readonly) == b'aZcd'
assert snapshot == b'abcd'

try:
    readonly[0] = ord('X')
except TypeError:
    print('readonly write: TypeError')
else:
    raise AssertionError('readonly write succeeded')

view.release()
try:
    buffer.append(ord('!'))
except BufferError:
    print('resize with readonly view: BufferError')
else:
    raise AssertionError('active view allowed resize')

readonly.release()
buffer.append(ord('!'))
print('buffer:', bytes(buffer))
try:
    bytes(readonly)
except ValueError:
    print('released view: ValueError')
else:
    raise AssertionError('released view remained usable')
assert buffer == b'aZcd!'

分别检查写入能力和内容稳定性

输出先显示 view: b'aZcd' 和 snapshot: b'abcd'。修改来自原 bytearray,只读视图照常观察到变化,而 bytes 快照仍保存修改前的内容。toreadonly 创建的是另一扇只读窗口,没有把被观察对象变成不可变对象。

通过只读视图赋值会得到 TypeError,这验证了接口确实限制了该入口的写操作。它并不提供跨线程一致性,也不会替其他写入者加锁。如果处理过程要求整段内容保持稳定,应复制数据、转移独占所有权,或使用明确的同步协议。

示例里的内容变化只替换一个字节,长度没有改变。append 则需要改变缓冲区大小,活跃的导出视图会让 bytearray 拒绝扩容并抛出 BufferError。不能把“允许修改已有元素”推导为“允许追加任意长度内容”,两种操作受到不同限制。

释放的是这扇窗口,不是所有窗口

先释放原始视图后,扩容依然失败,因为只读视图还在。释放第二个视图之后,追加才成功,输出 buffer: b'aZcd!'。真实程序还可能持有切片或传递出去的其他视图,应顺着引用关系检查,而不是只删除一个局部变量。

release 之后再访问该视图会得到 ValueError。提前释放有利于及时解除导出限制,但前提是调用链已经用完它。若函数返回一个视图,就不能在函数结束前释放返回对象;可以由调用者使用上下文管理器约定结束时机。

零拷贝也不意味着零内存成本。视图对象本身仍占空间,小切片还可能延长整个大缓冲区的生命周期。只打算长期保存很短的一段数据时,一次小复制可能比持有完整源缓冲区更划算,应结合真实对象大小和存活时间判断。

为接口选型时先写清三个问题:接收方能否修改、生产方何时复用、消费者要保存多久。只读视图适合限制当前入口的写入;不可变快照适合隔离后续变化。两者可以协作使用,但不能用一个 readonly 标记代替完整的数据生命周期设计。

参考资料

  1. Python 官方文档:memoryview、toreadonly 与 release

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。