Python 3.12 中推荐用 iter(partial(f.read, chunk_size), b'') 按块读取文件,需二进制模式、正确绑定参数并设合理块大小(如8192);处理UTF-8文本应使用 codecs.getreader 或 io.TextIOWrapper 避免字节截断。

Python 3.12 中用生成器按块读取文件,核心是避免一次性加载大文件到内存,同时保持代码简洁可控——iter() 配合 functools.partial 是最轻量、最符合 Python 惯例的做法,比手动 while 循环更安全,也比 readlines() + 切片更省内存。
用 iter() + partial 构造块读取生成器
这是官方推荐模式,利用 iter(callable, sentinel) 的双参数形式:每次调用 callable(如 f.read),直到返回值等于 sentinel(如空字节 b'')为止。关键在于把 f.read 绑定固定块大小。
常见错误是直接写 iter(f.read, b'')——这会传入未绑定的可调用对象,导致 TypeError;或者漏掉 sentinel,变成无限迭代。
- 必须用
functools.partial(f.read, chunk_size)绑定块大小,不能直接传f.read -
chunk_size推荐设为 8192 或 65536(2 的幂),兼顾系统页大小与缓存效率 - 文本文件需用
mode='rb',否则read()返回字符串,无法与b''比较
from functools import partial
<p>def read_in_chunks(file_path, chunk_size=8192):
with open(file_path, 'rb') as f:
for chunk in iter(partial(f.read, chunk_size), b''):
yield chunk</p><h1>使用示例</h1><p>for chunk in read_in_chunks('large.log'):
process(chunk) # chunk 是 bytes 类型
处理文本文件时如何避免字节边界截断中文
直接按字节切分会导致 UTF-8 编码的中文被劈开,解码失败(UnicodeDecodeError: invalid continuation byte)。不能靠“先读再 decode”,而要让 Python 自动处理编码边界。
立即学习“Python免费学习笔记(深入)”;
根本解法是不用 rb 模式 + iter,改用文本模式配合 io.BufferedReader 手动缓冲,或更简单:用 codecs.getreader 包装二进制流。
- 不要在生成器里对每个
chunk调用.decode('utf-8')—— 中文可能跨块 - 推荐方案:用
io.TextIOWrapper包装已有的二进制流,并设置line_buffering=False,再按行或按字符读(适合日志类场景) - 若真需“文本块”,可用
codecs.getreader('utf-8')(f),它内部维护解码状态,自动合并跨块字节
import codecs
<p>def read_text_chunks(file_path, chunk_size=8192):
with open(file_path, 'rb') as f:
reader = codecs.getreader('utf-8')(f)
while True:
chunk = reader.read(chunk_size)
if not chunk:
break
yield chunk
yield from 是否适合嵌套块生成器?
适合,但仅限于你已有多个独立的块生成器需要串联,且不关心中间 chunk 边界。例如合并多个分卷文件的块流。
容易踩的坑是误以为 yield from 能帮你“拼接”或“重分块”——它只是委托产出,不会合并或拆分。如果目标是把 4KB 块再聚合成 64KB 批处理,得自己累积 buffer,不能依赖 yield from。
-
yield from gen_a(); yield from gen_b()→ 输出是 a 的所有块 + b 的所有块,无交叉 - 若需动态重分块(如“每 5 个原始 chunk 合成一个 batch”),必须用显式 for 循环 + 计数/长度判断
- Python 3.12 对
yield from的性能优化不大,无需为此改写逻辑
为什么不用 pathlib.Path.read_bytes() 分块?
因为 read_bytes() 是全量读取接口,没有分块能力。它的存在意义是替代 open().read() 一行读完,和“流式分块”目标完全相反。
有人试图组合使用:Path(...).open('rb') 再套 iter —— 这可行,但绕路:既然都用 open 了,何必多套一层 Path?除非你已经在用 Path 管理路径逻辑(如检查 .exists()),否则纯增加对象创建开销。
-
Path.open()返回的 file object 和内置open()行为一致,可直接用于iter(partial(...)) - 但
Path.read_text()/read_bytes()都是原子操作,无法干预读取过程 - 在循环中反复调用
Path.open()(比如每块都 open 一次)是严重错误,会导致 OSError: Too many open files
真正要注意的是:块大小不是越大越好。超过 1MB 可能触发 Python 的大对象分配策略,反而降低 GC 效率;而低于 4KB 在多数 Linux 系统上无法对齐磁盘页,实际 I/O 放大。8KB 是平衡点,也是 CPython 默认 io.DEFAULT_BUFFER_SIZE 的值。


















