在 Python 中对字节对象进行切片是否会创建一个全新的数据副本?

Tek*_*ekz 12 python python-bytearray

假设我有非常大的字节对象(加载二进制文件后),我想逐部分读取并推进起始位置直到它到达末尾。我使用切片来实现这一点。我担心每次我请求切片时 python 都会创建全新的副本,而不是简单地给我指向我想要的位置的内存地址。

简单的例子:

data = Path("binary-file.dat").read_bytes()
total_length = len(data)
start_pos = 0

while start_pos < total_length:
   bytes_processed = decode_bytes(data[start_pos:])  # <---- ***
   start_pos += bytes_processed 
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,python 是否由于start_pos切片而从 开始创建全新的字节对象副本。如果是这样,避免数据复制并仅使用指针传递到字节数组的相关位置的最佳方法是什么。

use*_*170 5

是的,对 bytes 对象进行切片确实会创建一个副本,至少从 CPython 3.9.12 开始是这样。文档最接近承认这一点是在构造函数的描述中bytes

\n
\n

除了文字形式之外,字节对象还可以通过许多其他方式创建:

\n
    \n
  • 指定长度的零填充字节对象:bytes(10)
  • \n
  • 从整数的可迭代中:bytes(range(20))
  • \n
  • 通过缓冲区协议复制现有的二进制数据:bytes(obj)
  • \n
\n
\n

这表明任何字节对象的创建都会创建数据的单独副本。但由于我很难找到切片具有相同作用的明确证据,因此我诉诸于实证测试。

\n
>>> b = b\'\\1\' * 100_000_000\n>>> qq = [b[1:] for _ in range(20)]\n
Run Code Online (Sandbox Code Playgroud)\n

执行第一行后,python3进程的内存使用top量约为100 MB。第二个在相当大的延迟后执行,使得内存使用量上升到2G的水平。这看起来相当结论性的。针对 Python 3.8 的 PyPy 7.3.9 的行为基本相同;当然,PyPy\xe2\x80\x99s 垃圾收集不像 CPython\xe2\x80\x99s 那样急切,因此一旦对象bytes变得不可访问,内存就不会被释放。

\n

这是有道理的。如果切片创建了对原始缓冲区的引用而不是复制它,则可以通过分配一个大bytes对象,从中切片一个字节,然后删除对原始缓冲区的引用来创建内存泄漏。(V8 JavaScript 引擎曾经遇到过这个问题。)

\n

为了避免复制底层缓冲区,请将您的内容包装bytes在 amemoryview和 slice 中:

\n
>>> bm = memoryview(b)\n>>> qq = [bm[1:] for _ in range(50)]\n
Run Code Online (Sandbox Code Playgroud)\n