【Python Cookbook】文件与 IO(三)
文件与 IO(三)
- 9.读取二进制数据到可变缓冲区中(⭐⭐⭐)
- 9.1 readinto
- 9.2 memoryview
- 9.2.1 零复制
- 9.2.2 与传统方式的对比
- 传统方式(非零复制)
- 零复制方式(memoryview)
- 9.2.3 总结
- 10.内存映射的二进制文件(⭐⭐⭐)
9.读取二进制数据到可变缓冲区中(⭐⭐⭐)
你想直接读取二进制数据到一个可变缓冲区中,而不需要做任何的中间复制操作。或者你想原地修改数据并将它写回到一个文件中去。
9.1 readinto
为了读取数据到一个可变数组中,使用文件对象的 readinto()
方法。比如:
import os.pathdef read_into_buffer(filename):buf = bytearray(os.path.getsize(filename))with open(filename, 'rb') as f:f.readinto(buf)return buf
下面是一个演示这个函数使用方法的例子:
>>> # Write a sample file
>>> with open('sample.bin', 'wb') as f:
... f.write(b'Hello World')
...
>>> buf = read_into_buffer('sample.bin')
>>> buf
bytearray(b'Hello World')
>>> buf[0:5] = b'Hello'
>>> buf
bytearray(b'Hello World')
>>> with open('newsample.bin', 'wb') as f:
... f.write(buf)
...
11
>>>
文件对象的 readinto()
方法能被用来为预先分配内存的数组填充数据,甚至包括由 array
模块或 numpy
库创建的数组。
和普通 read()
方法不同的是, readinto()
填充已存在的缓冲区,而不是为新对象重新分配内存再返回它们。因此,你可以使用它来避免大量的内存分配操作。比如,如果你读取一个由相同大小的记录组成的二进制文件时,你可以像下面这样写:
record_size = 32 # Size of each record (adjust value)buf = bytearray(record_size)
with open('somefile', 'rb') as f:while True:n = f.readinto(buf)if n < record_size:break# Use the contents of buf...
使用 f.readinto()
时需要注意的是,你必须检查它的返回值,也就是实际读取的字节数。
如果字节数小于缓冲区大小,表明数据被截断或者被破坏了(比如你期望每次读取指定数量的字节)。
最后,留心观察其他函数库和模块中和 into
相关的函数(比如 recv_into()
, pack_into()
等)。Python 的很多其他部分已经能支持直接的 I/O 或数据访问操作,这些操作可被用来填充或修改数组和缓冲区内容。
9.2 memoryview
9.2.1 零复制
另外有一个有趣特性就是 memoryview
,它可以通过 零复制 的方式对已存在的缓冲区执行切片操作,甚至还能修改它的内容。
🚀 零复制(Zero-Copy)是一种 高效的数据处理技术,它允许程序 直接访问数据缓冲区,而无需在内存中复制数据。传统的数据操作(如切片、修改)通常需要先复制一份数据,而零复制技术避免了这一额外开销,从而提升性能并减少内存占用。
Python 的memoryview
对象提供了一种零复制的方式来操作现有的缓冲区(如bytes
、bytearray
、mmap
等),允许:
- 直接引用原始数据,而无需复制。
- 修改原始数据(如果底层缓冲区可写,如
bytearray
)。- 高效切片,即使操作大型数据也不会产生额外内存消耗。
比如:
>>> buf
bytearray(b'Hello World')
>>> m1 = memoryview(buf)
>>> m2 = m1[-5:]
>>> m2
<memory at 0x100681390>
>>> m2[:] = b'WORLD'
>>> buf
bytearray(b'Hello WORLD')
>>>
1. 创建 bytearray
缓冲区
buf = bytearray(b'Hello World') # 可变的字节数组
buf
是一个可修改的bytearray
,内容为b'Hello World'
。
2. 创建 memoryview
对象
m1 = memoryview(buf) # 零复制方式引用 buf
m1
是buf
的零复制视图,不复制数据,直接操作buf
的内存。
3. 对 memoryview
切片(零复制)
m2 = m1[-5:] # 获取最后 5 字节的视图(' World')
m2
是m1
的一个切片,仍然零复制,不会创建新数据副本。- 此时
m2
的内容是b'World'
(但底层仍指向buf
的对应部分)。
4. 通过 memoryview
修改原始数据
m2[:] = b'WORLD' # 修改 m2 的内容
- 将
m2
的内容替换为b'WORLD'
,由于m2
是零复制视图,直接修改了buf
。 - 最终
buf
变为bytearray(b'Hello WORLD')
。
9.2.2 与传统方式的对比
传统方式(非零复制)
buf = bytearray(b'Hello World')
sliced = buf[-5:] # 复制数据,生成新对象
sliced = b'WORLD' # 修改的是副本,不影响 buf
print(buf) # 仍为 b'Hello World'(未改变)
- 切片会复制数据,修改副本不影响原始数据。
零复制方式(memoryview)
buf = bytearray(b'Hello World')
m = memoryview(buf)
m[-5:][:] = b'WORLD' # 直接修改原始数据
print(buf) # 输出 b'Hello WORLD'
- 切片和修改均直接作用于原始数据。
9.2.3 总结
- 零复制:通过
memoryview
直接操作原始缓冲区,避免数据拷贝。 memoryview
的作用:- 提供对缓冲区的零复制访问。
- 支持高效切片和修改(若底层可写)。
- 适用场景:需要高性能、低内存占用的二进制数据处理任务。
10.内存映射的二进制文件(⭐⭐⭐)
你想 内存映射一个二进制文件到一个可变字节数组中,目的可能是为了随机访问它的内容或者是原地做些修改。
使用 mmap
模块来内存映射文件。下面是一个工具函数,向你演示了如何打开一个文件并以一种便捷方式内存映射这个文件。
import os
import mmapdef memory_map(filename, access=mmap.ACCESS_WRITE):size = os.path.getsize(filename)fd = os.open(filename, os.O_RDWR)return mmap.mmap(fd, size, access=access)
access
:访问模式,默认mmap.ACCESS_WRITE
(可读写),其他选项:
mmap.ACCESS_READ
(只读)mmap.ACCESS_COPY
(写操作不修改原文件,仅修改内存副本)os.path.getsize(filename)
:返回文件大小(字节数),用于确定内存映射的大小。os.open(filename, os.O_RDWR)
os.open()
比open()
更底层,返回文件描述符fd
(整数)。os.O_RDWR
:以读写模式打开文件。mmap.mmap(fd, size, access=access)
:
fd
:文件描述符。size
:映射的字节数(通常等于文件大小)。access
:访问权限(如mmap.ACCESS_WRITE
)。- 返回一个
mmap
对象,可以像操作bytes
或bytearray
一样操作文件数据。
为了使用这个函数,你需要有一个已创建并且 内容不为空 的文件。下面是一个例子,教你怎样初始创建一个文件并将其内容扩充到指定大小:
>>> size = 1000000
>>> with open('data', 'wb') as f:
... f.seek(size-1)
... f.write(b'\x00')
...
>>>
f.seek(N)
将文件指针移动到第N
个字节(从 0 开始计数)。- 这里移动到
size - 1
(即第 999,999 字节),因为我们要在最后写入 1 字节。b'\x00'
是一个值为 0 的字节(即空字节)。- 写入后,文件大小变为
size
(1,000,000 字节),未显式写入的部分会自动填充\x00
。
下面是一个利用 memory_map()
函数类内存映射文件内容的例子:
>>> m = memory_map('data')
>>> len(m)
1000000
>>> m[0:10]
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> m[0]
0
>>> # Reassign a slice
>>> m[0:11] = b'Hello World'
>>> m.close()>>> # Verify that changes were made
>>> with open('data', 'rb') as f:
... print(f.read(11))
...
b'Hello World'
>>>
mmap()
返回的 mmap
对象同样也可以作为一个上下文管理器来使用,这时候底层的文件会被自动关闭。比如:
>>> with memory_map('data') as m:
... print(len(m))
... print(m[0:10])
...
1000000
b'Hello World'
>>> m.closed
True
>>>
默认情况下,memeory_map()
函数打开的文件同时支持读和写操作。任何的修改内容都会复制回原来的文件中。
如果需要只读的访问模式,可以给参数 access
赋值为 mmap.ACCESS_READ
。比如:
m = memory_map(filename, mmap.ACCESS_READ)
如果你想在本地修改数据,但是又不想将修改写回到原始文件中,可以使用 mmap.ACCESS_COPY
:
m = memory_map(filename, mmap.ACCESS_COPY)
为了随机访问文件的内容,使用 mmap
将文件映射到内存中是一个高效和优雅的方法。例如,你无需打开一个文件并执行大量的 seek()
, read()
, write()
调用, 只需要简单的映射文件并使用切片操作访问数据即可。
一般来讲, mmap()
所暴露的内存看上去就是一个二进制数组对象。但是,你可以使用一个内存视图来解析其中的数据。比如:
>>> m = memory_map('data')
>>> # Memoryview of unsigned integers
>>> v = memoryview(m).cast('I')
>>> v[0] = 7
>>> m[0:4]
b'\x07\x00\x00\x00'
>>> m[0:4] = b'\x07\x01\x00\x00'
>>> v[0]
263
>>>
1. 内存映射文件
m = memory_map('data')
memory_map('data')
将文件data
映射到内存,返回一个mmap
对象m
,可以像bytes
一样操作。
2. 创建 memoryview
并转换为无符号整数('I'
)
v = memoryview(m).cast('I')
memoryview(m)
创建一个内存视图,允许以不同方式解释底层数据。.cast('I')
将数据解释为 无符号整数(unsigned int,4字节):'I'
是struct
模块的格式字符,表示 4 字节无符号整数(小端序)。- 此时
v
是一个无符号整数数组,可以直接通过索引访问/修改。
3. 修改无符号整数值
v[0] = 7
- 将第一个无符号整数(
v[0]
,对应m[0:4]
)设为7
。 - 写入后,
m[0:4]
的字节变为b'\x07\x00\x00\x00'
(小端序)。
4. 查看底层字节
m[0:4]
- 输出
b'\x07\x00\x00\x00'
,即:- 十六进制:
0x07 0x00 0x00 0x00
(小端序表示7
)。
- 十六进制:
5. 直接修改字节数据
m[0:4] = b'\x07\x01\x00\x00'
- 直接通过
mmap
对象修改前 4 字节为b'\x07\x01\x00\x00'
(小端序)。
6. 查看无符号整数值
v[0]
- 输出
263
,因为:b'\x07\x01\x00\x00'
在小端序下解析为0x00000107
=263
。
示例扩展
假设文件 data
初始内容为 b'\x00\x00\x00\x00\x00\x00\x00\x00'
(8字节全0):
# 映射文件并转换为无符号整数数组
m = memory_map('data') # 假设文件大小为 8 字节
v = memoryview(m).cast('I')# 修改前两个整数
v[0] = 7 # m[0:4] = b'\x07\x00\x00\x00'
v[1] = 263 # m[4:8] = b'\x07\x01\x00\x00'# 验证
print(m[0:8]) # 输出 b'\x07\x00\x00\x00\x07\x01\x00\x00'
print(v[0], v[1]) # 输出 7, 263
需要强调的一点是,内存映射一个文件并不会导致整个文件被读取到内存中。也就是说,文件并没有被复制到内存缓存或数组中。相反,操作系统仅仅为文件内容保留了一段虚拟内存。当你访问文件的不同区域时,这些区域的内容才根据需要被读取并映射到内存区域中。而那些从没被访问到的部分还是留在磁盘上。所有这些过程是透明的,在幕后完成!
如果多个 Python 解释器内存映射同一个文件,得到的 mmap
对象能够被用来在解释器直接交换数据。也就是说,所有解释器都能同时读写数据,并且其中一个解释器所做的修改会自动呈现在其他解释器中。很明显,这里需要考虑同步的问题。但是这种方法有时候可以用来在管道或套接字间传递数据。
这一小节中函数尽量写得很通用,同时适用于 Unix 和 Windows 平台。要注意的是使用 mmap()
函数时会在底层有一些平台的差异性。
另外,还有一些选项可以用来创建匿名的内存映射区域。如果你对这个感兴趣,确保你仔细研读了 Python 文档中 这方面的内容 。