Python 内存管理
有,而且如果把范围限定为 Python 后端开发中的“内存管理习惯”,我认为可以形成一套非常实用的规范。
先给你一个总原则:
Python 内存优化的核心,不是“主动释放内存”,而是“控制对象生命周期、减少不必要的对象创建、避免无意义的引用长期存在、控制峰值内存”。
尤其是你上一条提到的 self.xxx,其实正好属于其中非常重要的一类。
Python 的内存管理并不是简单的“变量出了作用域 → GC 立刻回收”。CPython 主要依赖引用计数,同时有循环垃圾回收机制处理引用环。Python 官方 gc 文档也明确说明了这一点。
下面我按照重要程度 + 实际开发价值来讲。
一、第一原则:优先控制“对象生命周期”
这是最重要的一条。
不要首先想着:
gc.collect()
而应该首先思考:
这个对象到底应该活多久?
例如:
def process(data):
result = step1(data)
result = step2(result)
return result
这是非常好的数据流。
因为:
data
↓
step1 result
↓
step2 result
↓
return
中间对象没有必要进入更大的生命周期。
不推荐把中间结果全部挂到 self
class Processor:
def process(self, data):
self.raw = data
self.cleaned = clean(data)
self.parsed = parse(self.cleaned)
self.result = build(self.parsed)
return self.result
如果 Processor 会长时间存在:
Processor
├── raw
├── cleaned
├── parsed
└── result
那么这些对象都可能跟着 Processor 一起活着。
更好的方式:
class Processor:
def process(self, data):
cleaned = clean(data)
parsed = parse(cleaned)
return build(parsed)
经验法则:
临时数据 → 局部变量
长期状态 → self.xxx
最终结果 → return
这条非常值得写进你的 AI Python 编码规范。
二、不要为了“面向对象”而保存所有状态
这是你刚才的问题进一步延伸。
错误理解:
“既然是类,就应该把数据都放到 self。”
正确理解:
对象应该保存“对象状态”,而不是保存“计算过程”。
例如:
class UserService:
def __init__(self, repository):
self.repository = repository
合理。
因为 repository 是 Service 长期依赖。
但是:
class UserService:
def create_user(self, data):
self.validated = validate(data)
self.normalized = normalize(self.validated)
self.result = build(self.normalized)
return self.result
没有必要。
更合理:
class UserService:
def create_user(self, data):
validated = validate(data)
normalized = normalize(validated)
return build(normalized)
这不仅内存更容易控制,而且:
- 状态更少
- 并发更安全
- 测试更简单
- 方法更容易理解
- 不容易产生隐藏依赖
三、不要创建不必要的数据副本
这是实际项目中特别重要的一点。
例如:
data = list(range(10_000_000))
new_data = list(data)
你可能瞬间产生两个大型容器:
data → 80 MB 左右
new_data → 80 MB 左右
当然,实际大小取决于元素类型。
很多时候你真正需要的是:
for item in data:
process(item)
而不是:
new_data = [item for item in data]
四、特别警惕
list()
、
dict()
、
set()
、切片带来的复制
例如:
new_data = list(data)
会创建新的列表容器。
new_data = data[:]
同样创建新的列表。
new_data = dict(data)
创建新的字典。
new_data = set(data)
创建新的集合。
所以看到:
list(...)
dict(...)
set(...)
data[:]
不要自动认为它们“只是类型转换”。
应该问:
这里真的需要复制吗?
五、大数据处理优先考虑迭代器 / 生成器
这是 Python 内存优化中非常重要的一条。
不推荐:
def get_users():
return [
transform(user)
for user in load_all_users()
]
如果有 100 万用户:
load_all_users
↓
100 万对象
↓
transform
↓
再创建一个 100 万对象列表
峰值内存可能非常高。
可以考虑:
def get_users():
for user in load_users():
yield transform(user)
然后:
for user in get_users():
process(user)
变成:
读取一个
↓
处理一个
↓
释放不再需要的引用
↓
读取下一个
这就是典型的流式处理。
六、但是不要“为了生成器而生成器”
例如:
sum(x for x in range(100))
很好。
但如果最终就是:
list(x for x in range(100))
那么你最终还是创建列表。
应该根据实际数据流决定。
核心不是:
“生成器一定比列表好。”
而是:
当数据量大且不需要一次性持有全部结果时,优先流式处理。
七、避免在循环中不断创建大型临时对象
例如:
for item in items:
result = expensive_transform(item)
process(result)
这通常没有问题。
因为 result 每轮都会被重新绑定。
但如果:
results = []
for item in items:
result = expensive_transform(item)
results.append(result)
那么你实际上是在告诉 Python:
把所有结果一直留着。
如果结果不需要全部保存,这就是内存问题。
可以改成:
for item in items:
result = expensive_transform(item)
process(result)
或者:
def process_all(items):
for item in items:
yield expensive_transform(item)
八、不要无意义地保留历史数据
这是非常常见的内存增长原因。
例如:
class Service:
def __init__(self):
self.history = []
def process(self, data):
result = process(data)
self.history.append(result)
如果这个 Service 长时间运行:
result 1
result 2
result 3
...
result 1,000,000
全部都被:
self.history
引用着。
这不是 Python GC 失效。
而是:
你自己明确告诉 Python:这些对象我还需要。
所以 GC 不应该释放。
九、缓存是内存管理的大坑
例如:
cache = {}
def get_user(user_id):
if user_id not in cache:
cache[user_id] = load_user(user_id)
return cache[user_id]
缓存当然有价值。
但是:
缓存 = 永久引用集合
如果没有限制:
100
1000
10000
100000
1000000
内存就可能不断增长。
所以缓存应该考虑:
- 最大容量
- TTL
- LRU
- 淘汰策略
- 是否真的需要缓存
- 缓存对象大小
例如使用有界缓存,而不是无限增长的 dict。
十、不要轻易使用全局变量保存大型对象
例如:
GLOBAL_DATA = load_huge_dataset()
那么:
GLOBAL_DATA
↓
巨大对象
整个进程生命周期内它都可能存在。
全局变量最大的特点就是:
生命周期非常长。
所以大型数据尤其需要谨慎。
十一、类属性也要特别小心
例如:
class Cache:
data = {}
这实际上可能变成:
进程
↓
Cache
↓
data
↓
所有缓存对象
类本身生命周期很长。
所以类属性特别适合:
MAX_RETRY = 3
TIMEOUT = 10
这种小型、稳定的配置。
而不应该随便:
class Manager:
data = []
然后不断:
Manager.data.append(...)
除非你明确设计它为全局共享状态。
十二、及时解除不再需要的大对象引用
有时候一个函数特别长:
def process():
large_data = load_huge_data()
step1(large_data)
step2(large_data)
# 后面再也不用
...
如果后面还有大量计算:
large_data = None
可以明确解除引用。
例如:
def process():
large_data = load_huge_data()
result = process_data(large_data)
large_data = None
do_other_expensive_work(result)
这在很长的函数 + 大型对象 + 明确生命周期的情况下有价值。
但是不要到处写:
data = None
temp = None
foo = None
普通短函数完全没必要。
十三、比
x = None
更好的方式通常是缩小作用域
例如:
def process():
large_data = load_data()
result = transform(large_data)
large_data = None
return expensive_calculation(result)
可以进一步设计成:
def load_and_transform():
large_data = load_data()
return transform(large_data)
def process():
result = load_and_transform()
return expensive_calculation(result)
这样:
load_and_transform()
↓
large_data 生命周期结束
↓
process()继续
缩小作用域通常比到处手动 = None 更优雅。
十四、慎重使用闭包捕获大型对象
例如:
def create_processor(large_data):
def process(item):
return transform(large_data, item)
return process
这里 process 闭包可能持有:
process
↓
closure
↓
large_data
即使外层函数:
create_processor()
已经结束,大对象也可能继续存活。
所以闭包非常方便,但如果闭包捕获大型对象,要意识到:
函数本身可能成为大型对象的长期引用者。
十五、注意异常 traceback 可能延长对象生命周期
例如复杂异常处理过程中:
try:
process(large_data)
except Exception:
...
异常对象、traceback、frame 等机制可能让一些局部对象在调试/异常处理场景下保持更久。
因此对于真正的大型数据处理系统,不应该:
长时间保存异常对象、traceback 或包含巨大局部变量的 frame。
尤其不要设计这种长期结构:
errors.append(exc)
然后:
exc
↓
traceback
↓
frame
↓
large_data
这可能间接保留很多你以为已经“不需要”的对象。
工程上更推荐记录:
errors.append(str(exc))
或者结构化记录:
errors.append({
"type": type(exc).__name__,
"message": str(exc),
})
而不是无限保存完整异常对象。
十六、循环引用要特别关注
例如:
class A:
def __init__(self):
self.b = None
class B:
def __init__(self):
self.a = None
a = A()
b = B()
a.b = b
b.a = a
形成:
a → b
↑ ↓
└───┘
即使外部:
a = None
b = None
内部仍然存在引用环。
这就是为什么 Python 不能只依赖简单的引用计数。
CPython 有循环垃圾回收机制处理这类不可达循环对象。
所以设计对象关系时:
能避免不必要的双向强引用,就尽量避免。
复杂对象图尤其需要注意。
十七、弱引用适合某些缓存/反向引用场景
如果你需要:
“我希望知道这个对象,但是不希望这个引用阻止对象被回收。”
可以考虑:
weakref
例如某些:
- 对象注册表
- 缓存
- observer
- parent/child 关系
- 反向引用
可以使用弱引用。
核心思想:
强引用:
A ─────→ B
A 活着 → B 也必须活着
而弱引用:
A - - - → B
A 的存在不会阻止 B 被回收
这是比较高级的内存管理技术,不应该为了“优化”到处使用。
十八、
__slots__
可以减少大量实例的内存开销
你之前问过这个,这里正好串起来。
普通类:
class User:
def __init__(self, name, age):
self.name = name
self.age = age
大量实例通常涉及实例字典。
如果对象数量极大,可以考虑:
class User:
__slots__ = ("name", "age")
def __init__(self, name, age):
self.name = name
self.age = age
这主要解决的是:
大量小对象的结构性内存开销。
例如:
10 个对象
通常没什么意义。
但是:
100 万 / 1000 万个对象
就可能非常重要。
所以:
__slots__ 是“大量实例”的优化工具,而不是默认编码规范。
十九、选择合适的数据结构
这也是非常重要的一点。
例如:
items = [1, 2, 3, 4, 5]
如果你只需要:
判断某个值是否存在
可能:
items = {1, 2, 3, 4, 5}
更合适。
但是不要简单理解成:
set 一定比 list 节省内存。
实际情况取决于:
- 元素类型
- 元素数量
- 哈希表容量
- 是否需要顺序
- 是否需要索引
- 是否需要重复元素
所以正确原则是:
根据访问模式选择数据结构,而不是单纯根据“谁更省内存”。
二十、字符串拼接也要关注临时对象
例如:
result = ""
for item in items:
result += str(item)
大型循环中可能产生大量中间字符串。
更推荐:
result = "".join(str(item) for item in items)
或者如果数据本身就是可流式输出的:
for item in items:
yield str(item)
当然,Python 对字符串拼接已经做了一些优化,所以不能简单说 += 一定很慢或一定产生完整复制;但在大量文本构建场景,join 通常是更明确的设计。
二十一、不要为了“内存优化”过早手动
gc.collect()
这是一个特别容易误入歧途的地方。
很多人发现:
处理大量数据
就写:
gc.collect()
甚至:
for item in items:
...
gc.collect()
这通常不是好习惯。
gc.collect() 是执行垃圾回收,而不是一个通用的“释放所有内存”按钮。Python 官方也说明了它主要针对垃圾回收器管理的不可达对象。
更重要的是:
如果还有强引用,gc.collect() 也不能释放你仍然需要的对象。
例如:
cache.append(large_object)
gc.collect()
没用。
因为:
cache
↓
large_object
仍然是可达对象。
二十二、不要把“Python 进程 RSS 没下降”直接理解成内存泄漏
这是非常重要的实战知识。
假设:
data = [huge objects]
del data
对象可能已经没有 Python 层面的引用。
但是:
Python 对象释放
↓
Python allocator
↓
内存可能保留在进程内部
↓
未来重复利用
所以:
对象被释放 ≠ 操作系统看到的 RSS 必然立即下降。
因此判断内存问题不能只看:
top
或者:
ps
就下结论。
二十三、真正遇到内存问题,要学会使用
tracemalloc
这是 Python 官方提供的内存分析工具。
它可以:
- 查看内存分配位置
- 按文件统计
- 按代码行统计
- 对比 snapshot
- 查找内存增长来源
官方文档明确说明,tracemalloc 可以追踪 Python 内存分配,并通过 snapshot 对比帮助定位内存泄漏。
例如:
import tracemalloc
tracemalloc.start()
# 运行你的代码
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics("lineno")[:10]:
print(stat)
这比:
print("内存怎么又变大了")
专业得多。
二十四、要关注“峰值内存”,而不仅仅是最终内存
这是很多开发者容易忽略的。
例如:
data = huge_data()
result = transform(data)
del data
最终可能:
result = 100 MB
但是处理过程中:
data 500 MB
result 500 MB
临时对象 300 MB
----------------
峰值 1300 MB
程序最终可能只剩:
500 MB
但已经因为峰值 1.3 GB:
OOM
所以真正应该关注:
Peak Memory,而不是仅仅最终存活对象。
tracemalloc 本身就提供当前追踪内存和峰值内存的统计能力。
二十五、数据库查询不要一次性加载全部数据
Python 后端尤其重要。
不推荐:
users = session.query(User).all()
如果数据库有:
500 万用户
你可能直接把大量数据搬进 Python 进程。
更合理的是:
- 分页
- batch
- cursor
- streaming
- chunk
- 数据库端聚合
例如:
数据库
↓
1000 条
↓
处理
↓
释放
↓
下一批
而不是:
数据库
↓
500 万条
↓
Python 内存
这实际上是后端开发中非常重要的“内存边界设计”。
二十六、文件不要无脑
read()
例如:
content = file.read()
如果文件:
2 GB
那么你就可能试图在 Python 中一次性持有 2 GB 数据。
更合理:
while chunk := file.read(1024 * 1024):
process(chunk)
或者:
for line in file:
process(line)
原则:
大输入 → 流式读取。
二十七、HTTP 响应也要注意流式处理
例如下载大文件:
response.content
可能把整个响应体加载到内存。
对于大文件:
stream
↓
chunk
↓
write
↓
chunk
↓
write
通常更合理。
尤其是:
- 文件服务
- 图片
- 视频
- 大 JSON
- 大 CSV
- 导入导出
二十八、JSON 是后端内存的大户
比如:
data = json.loads(huge_json)
这可能导致:
原始 JSON 字符串
+
Python dict/list/str/int 对象树
同时存在。
因此:
大 JSON 不应该默认“一次性 parse 全部”。
可以考虑:
- 分块
- streaming parser
- NDJSON
- 数据库分页
- 上游改变数据协议
这往往比“Python GC 优化”有效得多。
二十九、注意日志也可能造成内存问题
例如:
logs = []
for item in items:
logs.append(huge_debug_data(item))
最终:
logs
↓
所有历史日志
全部存在内存。
更合理:
logger.debug("processing item=%s", item_id)
或者写入外部日志系统。
不要为了调试:
debug_data.append(...)
然后生产环境一直积累。
三十、不要把大型对象放进默认参数
这是一个非常值得记住的 Python 陷阱。
虽然:
def func(data=[]):
...
首先是可变默认参数问题,但从生命周期角度也值得注意:
默认参数对象是在函数定义时创建并长期存在的。
因此:
def process(cache={}):
...
实际上这个 cache 会跟着函数对象长期存在。
正确:
def process(cache=None):
if cache is None:
cache = {}
三十一、慎重使用
lru_cache
例如:
from functools import lru_cache
@lru_cache
def process(data):
...
缓存参数和返回值都会影响生命周期。
如果返回值巨大:
key
↓
巨大 result
缓存就可能成为内存占用来源。
所以缓存要考虑:
@lru_cache(maxsize=128)
而不是:
@lru_cache(maxsize=None)
除非你非常确定它的生命周期和规模。
三十二、不要为了“减少变量”而牺牲生命周期设计
这一点和你最开始的问题有关。
有人可能为了:
“变量越少越省内存。”
写出:
return step3(step2(step1(data)))
这不一定更好。
有时候:
step1_result = step1(data)
step2_result = step2(step1_result)
return step3(step2_result)
反而更清晰。
变量数量本身不是核心问题。
真正重要的是:
引用关系
对象大小
对象生命周期
峰值内存
数据复制
所以不要陷入:
“变量少 = 内存好。”
三十三、
del
是工具,不是日常编码习惯
例如:
del large_data
确实可以删除当前命名空间中的引用。
但是:
del data
并不等于:
“释放 data 对象。”
只有当:
没有其他引用
时,对象才可能被回收。
例如:
a = large_object()
b = a
del a
对象仍然被:
b
引用。
所以:
del
的本质应该理解为:
删除一个引用,而不是直接销毁对象。
三十四、最好的“释放内存”往往是架构设计
这是我最想让你记住的一点。
如果你有:
10GB 数据
然后想:
gc.collect()
这通常已经是晚期处理。
真正好的设计是:
数据库
↓
分页 1000
↓
处理
↓
写入
↓
下一批
而不是:
数据库
↓
10GB
↓
Python
↓
gc.collect()
所以内存管理实际上和:
- API 设计
- 数据库设计
- 类设计
- 函数设计
- 并发设计
- 缓存设计
- 数据结构设计
全部有关。
最后给你一张“Python 内存管理习惯优先级”
如果你要把它变成自己的编码规范,我建议按照这个优先级:
|
优先级 |
习惯 |
重要程度 |
|---|---|---|
|
S |
控制对象生命周期 |
⭐⭐⭐⭐⭐ |
|
S |
不把临时数据无意义存进 |
⭐⭐⭐⭐⭐ |
|
S |
大数据流式处理 |
⭐⭐⭐⭐⭐ |
|
S |
避免无限缓存/历史列表 |
⭐⭐⭐⭐⭐ |
|
S |
数据库分页/批处理 |
⭐⭐⭐⭐⭐ |
|
S |
避免不必要的数据复制 |
⭐⭐⭐⭐⭐ |
|
A |
合理使用生成器/迭代器 |
⭐⭐⭐⭐ |
|
A |
避免大型全局变量 |
⭐⭐⭐⭐ |
|
A |
控制闭包捕获 |
⭐⭐⭐⭐ |
|
A |
控制异常对象/traceback 生命周期 |
⭐⭐⭐⭐ |
|
A |
合理设计对象关系,避免引用环 |
⭐⭐⭐⭐ |
|
A |
合理使用 |
⭐⭐⭐⭐ |
|
A |
限制缓存容量 |
⭐⭐⭐⭐ |
|
A |
大文件流式读写 |
⭐⭐⭐⭐ |
|
B |
必要时 |
⭐⭐⭐ |
|
B |
|
⭐⭐⭐ |
|
B |
|
⭐⭐ |
|
B |
微观变量数量优化 |
⭐ |
其中最重要的其实可以浓缩成一个 “Python 内存 8 条原则”:
1. 短生命周期的数据用局部变量。
2. 长生命周期的数据才放 self。
3. 不需要全部保存的数据不要组成 List。
4. 大数据优先流式处理、分页、分批。
5. 不要制造无意义的数据副本。
6. 缓存、全局变量、类属性必须有生命周期意识。
7. 优先通过缩小作用域控制生命周期,而不是到处 del。
8. 真遇到内存问题,用 tracemalloc 分析,不要凭感觉加 gc.collect()。
其中第 1、2、3、4、5 条,我认为特别值得加入你之前正在整理的 Python AI 编码规范。
而且你刚才从 return → 引用计数 → self 属性 → _ → 生命周期一路追问下来,其实已经串起了一条非常重要的知识链:
变量命名 → 引用关系 → 对象生命周期 → 内存峰值 → 类设计 → 函数设计 → 数据流设计 → 后端架构。
这比单独学习“Python 垃圾回收机制”更有实际工程价值。

浙公网安备 33010602011771号