一个字典居然占了200MB——Python对象的内存模型
先说结论
Python中一切皆对象——int、float、str甚至None都是对象。每个对象至少包含引用计数(8字节)和类型指针(8字节)=16字节的开销。一个int在C语言里只占4-8字节,在Python里占28字节。一个空dict占64字节。这就是为什么同样的数据Python比C/Java占用更多内存。优化方向:使用__slots__、用array/numpy替代list、用csv.reader替代dict存储。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
Python对象的内存布局
import sys
# 基本类型的内存占用
sys.getsizeof(0) # 28字节(int)
sys.getsizeof(1) # 28字节
sys.getsizeof(2**30) # 32字节(大整数多4字节)
sys.getsizeof(3.14) # 24字节(float)
sys.getsizeof(True) # 28字节(bool是int子类!)
sys.getsizeof(None) # 16字节
sys.getsizeof('') # 49字节(空字符串!)
sys.getsizeof('hello') # 54字节(5个字符的字符串)
# 容器类型的内存占用
sys.getsizeof([]) # 56字节(空list)
sys.getsizeof({}) # 64字节(空dict)
sys.getsizeof(set()) # 216字节(空set!)
sys.getsizeof(()) # 40字节(空tuple,比list小)
# 注意:getsizeof只统计对象本身,不统计引用的子对象
# dict {'a': 1} 的getsizeof不包含'a'和1的内存
为什么一个int占28字节?
# CPython中int对象的C结构体(简化版)
#
# typedef struct {
# Py_ssize_t ob_refcnt; // 引用计数 8字节
# PyTypeObject *ob_type; // 类型指针 8字节
# Py_ssize_t ob_size; // 数值长度 8字节(支持大整数)
# digit ob_digit[1]; // 实际数值 4字节
# } PyLongObject;
#
# 总计:8 + 8 + 8 + 4 = 28字节
# 而C语言的int只要4字节!
# Python的int比C的int大7倍
#
# 推广到所有对象:
# 每个Python对象至少有16字节的对象头
# ob_refcnt(8) + ob_type(8) = 16字节
# 再加上实际数据
dict的内存开销——为什么500万条占2.5GB?
# 500万条设备信息的dict
# devices = {
# 'device_001': {'model': 'iPhone 15', 'os': 'iOS', 'country': 'US'},
# 'device_002': {'model': 'Pixel 8', 'os': 'Android', 'country': 'JP'},
# ... # 500万条
# }
# 内存分析:
# 外层dict对象本身: ~300MB(500万个哈希槽+数据表)
# 每个key(str): ~60字节 x 500万 = ~300MB
# 每个value(内层dict): ~250字节 x 500万 = ~1.2GB
# 内层dict对象: ~200字节
# 内层的key(str): ~50字节 x 3 = ~150字节
# 内层的value(str): ~60字节 x 3 = ~180字节
# 总计: ~300 + 300 + 1200 + ... ≈ 2.5GB
# 原始CSV只有300MB
# Python对象开销导致内存膨胀了8倍!
内存优化方法
方法一:__slots__减少对象开销
# 普通class——每个实例有一个__dict__(约100字节)
class Device:
def __init__(self, model, os, country):
self.model = model
self.os = os
self.country = country
# sys.getsizeof(Device('x','y','z')).__dict__ ≈ 200字节/实例
# 使用__slots__——去掉__dict__,固定字段
class Device:
__slots__ = ('model', 'os', 'country')
def __init__(self, model, os, country):
self.model = model
self.os = os
self.country = country
# 约64字节/实例——省了2/3!
# 500万个实例:从1GB降到320MB
方法二:用Pandas/NumPy替代dict+list
# dict方式存500万条数据:约2.5GB
# Pandas DataFrame方式:
import pandas as pd
df = pd.read_csv('devices.csv',
dtype={'model': 'category', 'os': 'category', 'country': 'category'})
# category类型让重复字符串只存一份!
# 内存约200-400MB(节省80-90%)
# NumPy方式(纯数值数据):
import numpy as np
amounts = np.array([1.5, 2.3, 4.7, ...]) # float64数组
# 100万个float:NumPy占8MB,Python list占28MB
方法三:用namedtuple或dataclass替代dict
# namedtuple——不可变,比dict省内存
from collections import namedtuple
Device = namedtuple('Device', ['model', 'os', 'country'])
d = Device('iPhone 15', 'iOS', 'US')
# 约72字节/实例(比dict的200+字节省65%)
# dataclass(Python 3.7+)
from dataclasses import dataclass
@dataclass(slots=True) # Python 3.10+自动加__slots__
class Device:
model: str
os: str
country: str
引用计数和垃圾回收
Python的GC和Java不一样——主要靠引用计数(Reference Counting),辅以分代垃圾回收(Generational GC)处理循环引用:
| 机制 | 工作方式 | 优点 | 缺点 |
|---|---|---|---|
| 引用计数 | 每个对象记录有多少引用指向它,为0立即回收 | 实时回收,无暂停 | 无法处理循环引用 |
| 分代GC | 分三代,定期扫描找循环引用 | 处理循环引用 | 有暂停(但通常很短) |
大数据脚本中不太需要关心Python的GC——因为瓶颈通常在IO和CPU而不是GC。但如果你在循环里创建了大量临时对象(比如100万次字符串拼接),del手动删除不再需要的大对象可以帮助及时释放内存。
面试高频追问
追问一:Python的小整数缓存是什么?
CPython预先创建了-5到256的整数对象并缓存。a = 100; b = 100; a is b返回True(同一个对象)。a = 1000; b = 1000; a is b可能返回False(不同对象)。这就是为什么不要用is判断值相等——用==。小整数缓存的目的是减少频繁创建销毁小整数的开销。
追问二:Python的内存池(pymalloc)是什么?
CPython有自己的内存池管理小对象(小于512字节)。小对象不直接向OS申请内存,而是从Python内部的内存池中分配。好处是减少系统调用的开销和内存碎片。但释放的小对象内存不会立即归还给OS——这就是为什么你看到Python进程的内存使用只增不减(即使对象已经被回收了)。
追问三:怎么分析Python程序的内存使用?
三个工具:① sys.getsizeof()——单个对象的大小(不含子对象);② pympler.asizeof()——递归统计对象及所有子对象的总大小;③ memory_profiler——逐行分析内存变化(@profile装饰器+python -m memory_profiler script.py)。排查内存问题时memory_profiler最有用。
总结
Python内存面试讲清楚三层:① 一切皆对象的开销(int 28字节、空dict 64字节、对象头最少16字节);② 大数据场景的内存膨胀问题(500万dict从300MB CSV膨胀到2.5GB Python对象);③ 优化方法(__slots__/namedtuple省对象开销、Pandas category省字符串重复、NumPy省数值存储)。能讲出500万设备表的内存分析和优化方案,面试官会觉得你对Python内存有深度理解。
下一篇聊一个大数据面试的经典题:PySpark UDF为什么比Spark SQL慢10倍?GIL到底是什么?
你们Python脚本遇到过内存问题吗?怎么优化的?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,只讲大数据面试干货