文章目录

Python sys.getsizeof测量对象内存占用的准确性分析

发布于 2026-04-19 15:17:30 · 浏览 8 次 · 评论 0 条

Python sys.getsizeof测量对象内存占用的准确性分析

直接使用 sys.getsizeof 测量容器对象(如列表、字典)时,往往会得到一个远小于预期的数值。这是因为该函数默认只计算容器本身的内存开销,而不包含其引用对象的内存。以下将详细解析这一现象,并提供两种准确的测量方案。


1. 基础测试与误区验证

首先通过简单的代码观察 sys.getsizeof 的行为,验证其“浅测量”的特性。

打开 Python 交互式环境或编辑器,输入以下代码:

import sys

# 测试基础整数对象
empty_int = 0
large_int = 10**100

print(f"空整数占用: {sys.getsizeof(empty_int)} 字节")
print(f"大整数占用: {sys.getsizeof(large_int)} 字节")

# 测试列表对象
empty_list = []
small_list = [1, 2, 3]
large_list = [i for i in range(1000)]

print(f"空列表占用: {sys.getsizeof(empty_list)} 字节")
print(f"含3个元素的列表占用: {sys.getsizeof(small_list)} 字节")
print(f"含1000个元素的列表占用: {sys.getsizeof(large_list)} 字节")

观察输出结果,你会发现:

  • 整数的大小随着数值精度变化。
  • 列表 large_list 包含了1000个整数,但其占用的字节数仅比 empty_list 略大一点(通常只增加了数千字节,而非预期的数万字)。这表明 sys.getsizeof 仅计算了列表这个“容器”的结构体大小以及指针数组的开销,而没有计算指针指向的那1000个整数对象的实际大小。

为了更直观地理解这种内存布局,请参考下方的结构示意图。

graph LR subgraph Container["List 列表对象 (sys.getsizeof 的测量范围)"] direction TB C1["指针 0x01\n--> 元素 A"] --> A C2["指针 0x02\n--> 元素 B"] --> B C3["指针 0x03\n--> 元素 C"] --> C end subgraph Elements["独立的数据对象 (未被计入)"] A["整数对象 1\n占用 28 字节"] B["整数对象 2\n占用 28 字节"] C["整数对象 3\n占用 28 字节"] end Container -.->|sys.getsizeof\n仅返回此部分| Container

2. 深度测量方案一:递归计算函数

为了获得对象及其所有引用对象的总内存占用,我们需要编写一个递归函数来遍历对象的所有属性。这就像把一个集装箱里所有的箱子都打开,把里面的货物体积都加起来。

定义一个名为 get_total_size 的递归函数:

import sys
from collections.abc import Mapping, Container

def get_total_size(obj, seen=None):
    """
    递归计算对象及其引用对象的内存总大小
    """
    # 初始化已访问集合,防止循环引用导致死循环
    if seen is None:
        seen = set()

    # 获取对象ID
    obj_id = id(obj)

    # 如果对象已被计算过,直接返回0
    if obj_id in seen:
        return 0

    # 标记对象为已访问
    seen.add(obj_id)

    # 获取当前对象自身的内存大小
    size = sys.getsizeof(obj)

    # 如果是字典,遍历键和值
    if isinstance(obj, Mapping):
        size += sum(get_total_size(k, seen) + get_total_size(v, seen) for k, v in obj.items())

    # 如果是容器(如列表、元组、集合)但不是字符串(字符串也是容器但通常视为原子对象)
    elif isinstance(obj, Container) and not isinstance(obj, (str, bytes, bytearray)):
        try:
            size += sum(get_total_size(i, seen) for i in obj)
        except TypeError:
            # 处理某些无法迭代的容器情况
            pass

    return size

# 测试对比
test_list = [1, 2, [3, 4], {"a": 5}]

print(f"sys.getsizeof 结果: {sys.getsizeof(test_list)} 字节")
print(f"递归计算结果: {get_total_size(test_list)} 字节")

运行上述代码,递归计算结果 的数值将显著大于 sys.getsizeof 结果,因为前者包含了列表中的嵌套列表、字典以及所有整数的内存。


3. 深度测量方案二:使用 Pympler 库

手动编写递归函数容易出错,且处理循环引用逻辑复杂。在生产环境中,更推荐使用现成的第三方库 Pympler,它专门用于 Python 的内存分析和概要分析。

执行以下命令安装库:

pip install pympler

使用 asizeof 函数进行测量:

from pympler import asizeof
import sys

# 创建一个复杂的嵌套结构
complex_data = {
    "id": 12345,
    "tags": ["python", "memory", "test"],
    "payload": [{"x": i, "y": i*2} for i in range(100)]
}

# 对比测量
sys_size = sys.getsizeof(complex_data)
pympler_size = asizeof.asizeof(complex_data)

print(f"sys.getsizeof 测量值: {sys_size} 字节")
print(f"Pympler asizeof 测量值: {pympler_size} 字节")
print(f"差异倍数: {pympler_size / sys_size:.2f} 倍")

asizeof 函数内部已经实现了完善的递归逻辑和垃圾回收器头信息的处理,能够精准返回对象占用的全部系统内存。


4. 结果对比与数据参考

下表展示了在不同数据结构下,标准库 sys.getsizeof 与深度测量方法的数值差异。

数据结构内容 sys.getsizeof<br>(仅容器) 递归/Pympler<br>(含引用对象) 准确度说明
int 1000000 28 28 无引用,数值一致
str "hello"*100 514 514 无引用,数值一致
list [1, 2, 3] 88 168 前者忽略3个整数的占用
dict {k: v for k,v in zip(range(100), range(100))} 4648 9232 字典结构开销大,且包含键值对对象
list [[1,2]] * 1000 8056 56056 大量重复引用同一子列表,递归算法需处理去重

5. 关键注意事项

在使用深度测量工具时,必须注意以下两个核心概念,它们会直接影响测量结果的解释。

理解 虚引用与共享内存。
如果一个列表 L = [1, 1, 1] 包含三个相同的整数 1,在内存中这三个指针可能指向同一个整数对象(取决于 Python 的整数缓存机制)。深度测量时,是否会重复计算这个 1 的内存?

  • sys.getsizeof:不计算引用,无所谓。
  • 递归/Pympler:通常需要利用 id() 缓存机制(如方案一中的 seen 集合)来避免重复计算同一个对象。如果算法不加 seen 集合,共享对象会被重复计入;如果加了,共享对象只计一次。方案一和方案二默认都去除了重复计数,反映的是“去重后的物理内存占用”。

警惕 循环引用。
如果对象 A 引用 B,B 又引用 A(例如双向链表或树结构),简单的递归会陷入死循环。

  • 确保 你的测量代码(无论是手写还是使用库)包含了对象 ID 记录机制(即 seen 集合),在进入递归前检查 id(obj) 是否已存在。
# 循环引用示例
a = []
b = [a]
a.append(b)

# 无去重机制的递归将导致 RecursionError 或错误的数据
# 使用 Pympler 或方案一的代码则可安全处理
print(f"含循环引用的安全测量: {asizeof.asizeof(a)} 字节")

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文