不同Pandas DataFrame复制方式的内存消耗差异及疑问
一、测试结果疑惑的原理解析
1. 第12行与第13行内存增量差异
第一次创建df时,除了数据本身的内存开销,还包含pandas内部初始化的额外成本(如全局索引对象、数据类型缓存、模块加载等),这部分开销仅在首次创建DataFrame时产生。第二次创建df2时,这些初始化资源已存在,因此仅新增数据相关的内存,增量与初始列表a的内存大小一致。
2. 第15行增量异常的解读
由于func2和func都被@profile装饰,memory_profiler会分别统计两个函数的内存变化。在func的统计结果中,第15行的增量显示异常是工具的统计逻辑问题:它错误地将func2函数的起始内存作为基准,而非func函数的当前内存状态。实际增量应与func2内部的增量(0.012 MiB)一致,仅对应函数调用的微小开销。
3. 深拷贝与修改未显式增加内存的原因
你的pandas版本(1.4.3)虽未默认开启写时复制(Copy-On-Write),但内部对深拷贝做了延迟优化:
- 执行
df.copy(deep=True)时,并未立即复制底层numpy数组,而是共享原数据的内存; - 仅当执行
df6.loc[:,"val"] = 2修改数据时,才会触发实际的内存复制。但memory_profiler依赖的进程RSS统计无法精确捕捉这种延迟分配的内存,且numpy数组的整列赋值属于in-place修改,未触发新内存分配,导致增量不明显。
4. copy.deepcopy的内存异常
copy.deepcopy对DataFrame的处理本质上依赖pandas自身的深拷贝实现,因此同样受上述延迟复制优化的影响。同时,memory_profiler的RSS统计局限性,导致无法准确反映递归复制带来的内存变化。
二、不同复制方式的内存消耗差异对比
| 复制方式 | 内存消耗特点 | 数据共享情况 |
|---|---|---|
直接赋值(df4 = df) | 无额外内存消耗,仅增加对象引用 | 完全共享所有数据与结构,修改互影响 |
浅拷贝(df.copy(deep=False)) | 仅消耗元数据(索引、列名等)的微小内存 | 共享底层数据数组,修改数据会影响原DataFrame |
pandas深拷贝(df.copy(deep=True)) | 未修改时几乎无额外内存,修改后消耗与原DataFrame数据量相等的内存 | 未修改时共享数据,修改后独立拥有数据 |
copy.deepcopy(df) | 理论上消耗与原DataFrame完全相等的内存,实际受pandas优化影响,未修改时延迟分配 | 递归复制所有对象,最终数据完全独立 |
三、内存消耗研究的替代方法
1. pandas自带memory_usage()
直接计算单个DataFrame的真实内存消耗,支持深度统计对象类型数据:
import pandas as pd df = pd.DataFrame({"val": [1]*(10**6)}) # 统计包括底层数据在内的总内存 total_mem = df.memory_usage(deep=True).sum() / 1024 / 1024 print(f"DataFrame总内存:{total_mem:.2f} MiB")
2. Python标准库tracemalloc
精确跟踪内存分配的细节,对比不同操作的内存变化:
import tracemalloc import pandas as pd from copy import deepcopy tracemalloc.start() # 初始状态 a = [1]*(10**6) df = pd.DataFrame({"val": a}) snapshot_init = tracemalloc.take_snapshot() # 深拷贝 df_deep = df.copy(deep=True) snapshot_deep = tracemalloc.take_snapshot() # 修改深拷贝后的DataFrame df_deep.loc[:, "val"] = 2 snapshot_modify = tracemalloc.take_snapshot() # 输出内存变化 print("深拷贝后的内存差异:") for stat in snapshot_deep.compare_to(snapshot_init, 'lineno')[:5]: print(stat) print("\n修改后的内存差异:") for stat in snapshot_modify.compare_to(snapshot_deep, 'lineno')[:5]: print(stat)
3. sys.getsizeof结合numpy数组nbytes
手动计算DataFrame的总内存(包含底层数组):
import sys import pandas as pd df = pd.DataFrame({"val": [1]*(10**6)}) # DataFrame对象本身的内存 + 所有列数组的内存 total_mem = sys.getsizeof(df) + sum(col.values.nbytes for col in df.columns) print(f"总内存:{total_mem / 1024 / 1024:.2f} MiB")
4. pympler库的asizeof
递归计算对象的总内存,包含所有嵌套对象:
from pympler import asizeof import pandas as pd df = pd.DataFrame({"val": [1]*(10**6)}) print(f"总内存:{asizeof.asizeof(df) / 1024 / 1024:.2f} MiB")
四、关于psutil统计的补充说明
memory_profiler依赖psutil获取进程RSS(常驻集大小),而RSS包含操作系统缓存的内存,无法精确反映Python对象的实际内存占用。在OS X和Ubuntu上的一致结果并非psutil的bug,而是RSS统计本身的局限性——它无法区分共享内存、延迟分配内存与实际对象占用的内存。
内容的提问来源于stack exchange,提问作者jjei

