shutil.copy() 会吃光内存是因为默认将整个源文件读入内存再写入,解决方法是用 shutil.copyfileobj() 手动分块复制并指定 length 参数控制缓冲区大小,需以二进制模式打开文件。

为什么 shutil.copy() 会吃光内存?
直接调用 shutil.copy() 复制几个 GB 的文件时,Python 可能瞬间占用数 GB 内存——它默认把整个源文件读进内存再写出去。这不是 bug,是设计使然:shutil.copy() 底层用 shutil.copyfileobj(),而后者默认使用 io.DEFAULT_BUFFER_SIZE(通常 8192 字节),但若你传入的是非缓冲的文件对象或某些封装流(比如 gzip.GzipFile),就可能触发全量加载。
真正踩坑的点在于:你以为自己在“流式复制”,其实底层悄悄缓存了全部数据。
手动分块复制:用 shutil.copyfileobj() 控制缓冲区大小
核心是绕过 shutil.copy() 的封装,直接调用 shutil.copyfileobj() 并显式指定 length 参数。这个参数就是每次读写的字节数,也是控制内存占用的关键。
- 设为
64 * 1024(64KB)适合大多数机械盘和 SSD,平衡 I/O 次数与内存占用 - 低于
8192会显著增加系统调用次数,拖慢速度;高于1024 * 1024(1MB)对多数场景收益递减,且单次分配更大内存块 - 必须用二进制模式打开文件:
open(src, "rb")和open(dst, "wb"),否则报TypeError: a bytes-like object is required
import shutil <p>def copy_large_file(src, dst, chunk_size=65536): with open(src, "rb") as fsrc: with open(dst, "wb") as fdst: shutil.copyfileobj(fsrc, fdst, length=chunk_size) </p>
复制时需要校验或进度反馈?别改 copyfileobj(),封装一层
shutil.copyfileobj() 本身不提供进度回调,强行在循环里加 print 会影响性能,也破坏复用性。更稳妥的做法是自己实现一个兼容接口的“增强版 copyfileobj”:
- 保持参数签名一致,便于替换调试
- 用
fsrc.seek(0, 2)获取总大小前先fsrc.tell()记下当前位置,避免影响后续读取 - 进度回调函数接收当前已复制字节数,不建议在回调里做耗时操作(如写磁盘日志)
def copyfileobj_with_callback(fsrc, fdst, callback=None, length=65536):
total = 0
while True:
buf = fsrc.read(length)
if not buf:
break
fdst.write(buf)
total += len(buf)
if callback:
callback(total)
注意 Windows 下的 os.replace() 替换风险
如果你在复制后想原子化地替换目标文件(例如热更新配置),别直接用 os.replace(dst, target) —— 若 dst 和 target 在不同磁盘分区,os.replace() 会退化为复制+删除,再次触发大内存占用。
安全做法:
- 先用
shutil.disk_usage()检查目标路径所在磁盘剩余空间是否足够 - 确认
dst和target在同一驱动器:用os.path.splitdrive(dst)[0] == os.path.splitdrive(target)[0] - 仅当同盘时才用
os.replace();否则老实用shutil.move(),它内部会自动降级处理
大文件分块复制看似只是改个函数调用,但缓冲区大小、文件打开模式、跨盘移动逻辑这三点,漏掉任一个都可能让“省内存”变成“更卡”。尤其在容器或低内存设备上,length 值不是越大越好,得结合实际 I/O 能力测出来。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











