视图、副本与内存模型
本教程共 54 篇 · 第 10 篇 · 更新于 2026-08-11 · 约 7 分钟阅读
本节目标:分清视图和副本的区别,知道什么时候必须 copy(),学会用 memory_usage 和 info 估算内存,理解 DataFrame 的列式存储结构。
视图 vs 副本:共享还是独立
先记住两个词的定义:
- 视图(view):和原对象共享底层数据。改视图,原对象跟着变;原对象变,视图也变。
- 副本(copy):有独立的数据。两边互不影响。
上一节讲过,pandas 3.0 之前,df["foo"] 这类操作返回的就是视图,修改它会产生副作用。这也是 CoW 要解决的问题。
现在可以回答一个常见疑问:CoW 开启后,视图还存在吗?
存在,但被”藏”起来了。从外部看,任何派生对象都表现得像独立副本——改它不会波及原对象;而内部,pandas 会尽量让它们共享数据,等真正修改的那一刻才复制。这种机制叫懒复制(lazy copy)。
也就是说:
- 行为上:视图约等于副本,安全
- 内存上:共享数据的视图约等于零成本,高效
两全其美。你不需要关心某个操作返回的是视图还是副本,只需要在明确要独立数据时调用 .copy()。
copy():何时必须显式复制
.copy() 方法返回一个真正独立的副本:
import pandas as pd
import numpy as np
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
df2 = df.copy() # 独立副本
df2.iloc[0, 0] = 100
print(df.iloc[0, 0]) # 1,原对象毫发无损
注意一个细节:copy() 默认复制数据,索引对象(index/columns)也会新建一份,只是内容相同。好在索引不可变(§08 讲过),无论共享还是各自持有都安全:
print(df2.index.equals(df.index)) # True,内容相同
哪些场景必须显式 copy?给你三个典型:
- 把 DataFrame 传给一个会修改参数的函数,且你想保留原件
- 对取出的子集做实验性修改,又不希望影响原数据
- 从
to_numpy()拿到只读数组后,想先复制再改(§09 提过)
其他时候,CoW 已经替你兜底,不必到处 copy。官方原话是”很少有必要复制对象”——每行代码都 copy 反而浪费内存、拖慢速度。
Tip判断是否需要 copy 的标准很简单:这个对象以后还会不会用到?会,且可能被改,就 copy;不会,放心传。
copy 参数:构造函数的默认行为
很多构造函数带 copy 参数,比如 pd.Series(data, copy=...)、pd.DataFrame(data, copy=...)。3.0 有个重要变化:传入 NumPy 数组时,默认复制。
arr = np.array([1, 2, 3])
s = pd.Series(arr) # 默认 copy=True:创建时就复制
arr[0] = 999 # 改原数组
print(s.iloc[0]) # 1,Series 不受影响
为什么要改默认行为?以前默认不复制,你在 pandas 外面对数组做了原地修改,pandas 对象会莫名其妙跟着变,排查起来很痛苦。现在默认复制,切断了这条隐式关联。
如果你确定数组不会再被外部修改,可以显式 copy=False 省一次复制:
s = pd.Series(arr, copy=False) # 明确共享,性能更好
Warning用
copy=False时,数组和 Series 共享内存。之后任何一方被原地修改,另一方都会变。这是有意为之,出了问题别怪 pandas。
用 memory_usage 估算内存
想知道一个 DataFrame 占多少内存,用 memory_usage():
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
print(df.memory_usage(deep=True))
输出是按列统计的字节数(含 Index 行):
Index 132
a 24
b 24
dtype: int64
默认统计的是”浅”内存,字符串这类对象只算指针大小。想看真实占用,必须 deep=True,它会把字符串、分类等对象的实际内容也算进去:
df2 = pd.DataFrame({"名字": ["张三", "李四", "王五"],
"分数": [85, 92, 78]})
print(df2.memory_usage(deep=True))
这时”名字”列会统计每个字符串真实的字节数,比浅统计大得多。
memory_usage(deep=True) 也是做内存优化的第一步:先量出每列占多少,再对症下药。比如低基数的字符串列(取值很少)转成 category 类型,内存能省一个数量级,这在 §50 会细讲。
另一种姿势:info(memory_usage=“deep”)
df.info() 也能显示内存占用,加 memory_usage="deep" 就能看到真实值:
df = pd.DataFrame({"a": [1, 2, 3], "b": ["x", "y", "z"]})
df.info(memory_usage="deep")
输出里会有一行 memory usage: xxx bytes。info 顺带展示行数、列数、每列非空个数和 dtype,排查数据问题时比单独调 memory_usage 更全面,日常查看推荐用这个。
底层结构:列式存储
pandas 的 DataFrame 在底层是按列存储的:每一列是一个独立的一维数组,所有列共享同一个行索引。
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
print(df["a"].array) # 一个独立的数组
print(df["b"].array) # 另一个数组
这就是列式(column-oriented)存储。和按行存储的数据库相比,它的好处很直接:
- 取一列就是拿一个连续数组,速度快
- 同类型的数据放一起,压缩和计算效率高
- 只用某几列时,其他列可以不读入内存
历史上,pandas 用一套叫 BlockManager 的内部机制管理这些数组。它把相同 dtype 的列合并成”块”(block),统一存储和计算。比如三列 float64 会放进同一个二维块里,既省内存又加速。
pandas 3.0 的存储层正在向 Apache Arrow 格式演进,列式思想不变,但底层数组换成了更统一的 Arrow 结构,跨语言交换、内存效率都更好。作为使用者,你基本感知不到这些变化。只需要记住一句话:DataFrame 是一组列数组 + 一个共享索引的组合体。
Note这也解释了为什么 pandas 处理”宽数据”(列很多)比”高数据”(行很多)更费内存:每一列都有自己的数组开销。列越多,固定开销越大。
小结
- 视图共享数据、副本独立数据;CoW 下派生对象行为上都像副本,内部仍共享
.copy()默认复制数据,索引对象也会新建一份(内容相同);索引不可变,怎么操作都安全- 3.0 构造函数默认复制 NumPy 数组,
copy=False可显式跳过 memory_usage(deep=True)按列统计真实内存占用,info(memory_usage="deep")更全面- DataFrame 底层是列式存储:每列一个数组 + 共享索引;BlockManager 按 dtype 分块管理,3.0 逐步转向 Arrow 存储
下一节开始进入数据 IO 的世界,第一站是最常用的 CSV 文件读写。