当前位置: 首页 > news >正文

【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 对象提供了一种零复制的方式来操作现有的缓冲区(如 bytesbytearraymmap 等),允许:

  1. 直接引用原始数据,而无需复制。
  2. 修改原始数据(如果底层缓冲区可写,如 bytearray)。
  3. 高效切片,即使操作大型数据也不会产生额外内存消耗。

比如:

>>> 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
  • m1buf 的零复制视图,不复制数据,直接操作 buf 的内存。

3. 对 memoryview 切片(零复制)

m2 = m1[-5:]  # 获取最后 5 字节的视图(' World')
  • m2m1 的一个切片,仍然零复制,不会创建新数据副本。
  • 此时 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 对象,可以像操作 bytesbytearray 一样操作文件数据。

为了使用这个函数,你需要有一个已创建并且 内容不为空 的文件。下面是一个例子,教你怎样初始创建一个文件并将其内容扩充到指定大小:

>>> 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 文档中 这方面的内容 。

相关文章:

  • [Linux] Linux 系统从启动到驱动加载
  • 启动你的RocketMQ之旅(七)-Store存储原理
  • Linux系统配置Docker镜像加速
  • 定时任务:springboot集成xxl-job-core(二)
  • [学习] PID算法原理与实践(代码示例)
  • 彻底理解Spring三级缓存机制
  • 助力高校AI教学与科研:GpuGeek推出618算力支持活动
  • SAP学习笔记 - 开发18 - 前端Fiori开发 应用描述符(manifest.json)的用途
  • 【python基础知识】字典
  • C++多重继承详解与实战解析
  • 编程基础:通信
  • [SAP] 矩阵复制(Matrix Copy)
  • Linux开发追踪(IMX6ULL篇_第一部分)
  • 智语心桥:当AI遇上“星星的孩子”,科技如何点亮沟通之路?
  • 【办公类-22-05】20250601Python模拟点击鼠标上传CSDN12篇
  • 机器学习有监督学习sklearn实战二:六种算法对鸢尾花(Iris)数据集进行分类和特征可视化
  • 核心机制:TCP 断开连接(四次挥手)
  • 人工智能在智能能源管理中的创新应用与未来趋势
  • springboot中@Async做异步操作(Completable异步+ThreadPoolTaskExecutor线程池+@Async注解)
  • Leetcode 269. 火星词典
  • 什么网站可以接设计方案/网络营销的重要性与意义
  • 网站开发学习/品牌宣传推广策划方案
  • 专业网站定制服务/全网营销推广服务
  • 做网站技术好学嘛/google搜索网址
  • 网站备案关闭影响排名/廊坊网络推广优化公司
  • 自动登录网站的小程序/百度关键词搜索怎么收费