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

不把临时数据无意义存进 self

⭐⭐⭐⭐⭐

S

大数据流式处理

⭐⭐⭐⭐⭐

S

避免无限缓存/历史列表

⭐⭐⭐⭐⭐

S

数据库分页/批处理

⭐⭐⭐⭐⭐

S

避免不必要的数据复制

⭐⭐⭐⭐⭐

A

合理使用生成器/迭代器

⭐⭐⭐⭐

A

避免大型全局变量

⭐⭐⭐⭐

A

控制闭包捕获

⭐⭐⭐⭐

A

控制异常对象/traceback 生命周期

⭐⭐⭐⭐

A

合理设计对象关系,避免引用环

⭐⭐⭐⭐

A

合理使用 __slots__

⭐⭐⭐⭐

A

限制缓存容量

⭐⭐⭐⭐

A

大文件流式读写

⭐⭐⭐⭐

B

必要时 del / = None

⭐⭐⭐

B

weakref

⭐⭐⭐

B

gc.collect()

⭐⭐

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 垃圾回收机制”更有实际工程价值。

posted @ 2026-08-12 11:52  zwx901323  阅读(5)  评论(0)    收藏  举报