Python 的内存管理通常不需要手动干预,但在处理大量对象、长时间运行的服务或内存敏感的应用时,有几种简单有效的优化策略可以显著降低内存占用。本节介绍三个最实用的手段:对象复用、弱引用和 slots。
对象复用
创建对象需要分配内存,频繁创建和销毁大量相同或相似的对象会增加内存碎片和 GC 压力。对象复用就是让已有的对象被重复使用,避免不必要的重新分配。
小整数和短字符串自动复用
Python 内部已经为你做了一部分复用:
- 小整数对象池:CPython 启动时会预先创建 -5 到 256 之间的整数对象,之后任何地方使用这些值都直接引用同一对象,不再新建。
- 短字符串驻留(interning):由字母、数字或下划线组成的标识符样式的字符串(以及在编译期确定的字符串常量)会被驻留,相同值的字符串共享同一对象。
a = 100
b = 100
print(a is b) # True,因为复用了小整数对象池中的同一个对象
s1 = "hello"
s2 = "hello"
print(s1 is s2) # True,字符串驻留
你需要知道的:写代码时不需要刻意利用这一点,但理解了可以避免一些 is 比较的误判。另外,对于运行时动态生成的字符串,可以显式使用 sys.intern() 强制驻留,适合处理大量重复字符串的场景(如文本解析中的标签名)。
手动复用:对象池与缓存
- 对象池模式:维护一个可复用对象的集合,需要时从中取出,用完归还。例如数据库连接池、线程池就是这种思想。
- 缓存计算结果:对于创建成本高的对象(如加载大文件、计算复杂结构),把结果缓存起来重复使用,而不是每次重新计算。
functools.lru_cache可以方便地实现函数级缓存。
from functools import lru_cache
@lru_cache(maxsize=128)
def load_config(file_path):
# 昂贵的加载操作
return open(file_path).read()
数值计算中的向量化复用
在 NumPy 等库中,尽量使用向量化操作代替 Python 循环,本质上是将循环创建临时对象的开销交由底层 C 实现批量处理,减少了 Python 对象的生成次数。
弱引用
问题:引用计数与循环引用
Python 主要靠引用计数管理内存。当对象 A 引用对象 B,B 的引用计数就加 1;计数归零时 B 被销毁。但如果 A 和 B 互相引用,即使外部不再需要它们,引用计数也无法归零,就形成了循环引用。虽然垃圾回收器能处理循环引用,但它不是实时的,而且大量循环引用会拖慢回收速度、占用更多内存。
弱引用的作用
弱引用(weakref)是一种不增加引用计数的引用方式。你可以通过弱引用“观察”某个对象,但不会阻止它被回收。当对象只剩下弱引用时,它可以被正常销毁。
- 常用于缓存:缓存应该持有对象的弱引用,这样当对象在其他地方不再使用时,缓存不会成为它无法释放的罪魁祸首。
- 也用于观察者模式或回调注册:注册的回调持有目标对象的弱引用,避免因为注册关系导致目标对象无法释放。
常用工具
weakref.ref(obj):创建单个弱引用,调用它返回被引用对象(如果对象还存在),否则返回None。weakref.WeakValueDictionary:一种字典,它的值是对对象的弱引用。当某个值对象不再被其他地方强引用时,对应的键值对会自动从字典中消失。非常适合实现缓存。weakref.WeakSet、weakref.WeakKeyDictionary类似。
示例:用 WeakValueDictionary 做一个简易缓存
import weakref
class Image:
def __init__(self, path):
self.path = path
# 模拟加载图片数据(比较耗内存)
self.data = self._load()
def _load(self):
return f"data of {self.path}"
class ImageCache:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_image(self, path):
img = self._cache.get(path)
if img is None:
img = Image(path)
self._cache[path] = img
return img
cache = ImageCache()
img = cache.get_image("a.png") # 创建并缓存
# 当 img 变量被删除或重新赋值后,如果外界没有其他强引用,Image 对象会被回收,
# 对应缓存条目也会自动消失。
注意:弱引用只能用于类的实例、函数、方法等,对于内置类型(如 list、dict、int、str)不能直接创建弱引用,但可以通过子类化来间接实现。
slots 节省内存
默认情况下,每个类的实例都有一个 dict 属性,它是一个字典,用于存储所有实例属性。字典虽然灵活(可以动态添加属性),但内存开销很大。当你需要创建成千上万个实例时,每个实例的字典会消耗大量内存。
slots 是一种类级别的声明,告诉 Python:“这个类的实例只会拥有这些固定属性,不要创建 dict”。实现原理是为实例分配一个紧凑的 C 风格数组来存储属性值,而不是为每个实例维护一个字典。
用法
在类中定义 slots = ('attr1', 'attr2', ...),其中元素是字符串,表示允许的属性名。
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.x, p.y) # 正常运行
# p.z = 3 # AttributeError: 'Point' object has no attribute 'z'
内存节省效果:创建一个普通类实例(有 dict)的开销约 64 字节起(仅对象开销,不含属性),再加上字典本身的开销。使用 slots 后,每个实例只包含数组空间,通常可节省 50%~70% 的内存,具体取决于属性数量。在需要创建数百万个坐标点、日志记录等简单数据对象时,效果非常明显。
注意事项与限制
slots只对定义它的类生效,不会继承给子类。如果子类也定义了slots,则子类实例会有自己的属性数组 + 父类的属性数组;如果子类没有定义slots,则子类实例会重新获得dict,失去内存优化效果。- 定义了
slots的类不再支持动态添加dict中不存在的属性(这正是目的),但可以显式在slots中包含'dict'来恢复动态属性能力,不过这样内存节省效果就没那么明显了。 - 不支持多继承中同时使用具有非空
slots的父类(Python 会报错),因为内存布局会发生冲突。 slots是个类变量,通常用元组定义;也可以使用列表,但不推荐,因为列表可变但在这里没有意义。
适用场景
- 数据类:如
namedtuple的替代方案,或者只需存储几个字段的记录对象。 - 大量实例的场景:如游戏中子弹、粒子,金融交易记录,日志条目等。
- 配合
dataclasses使用:Python 3.10+ 支持@dataclass(slots=True),自动生成slots。
与其他优化的关系:slots 解决的是“每个实例的元信息字典”带来的内存浪费,而对象复用和弱引用的关注点是避免不必要的对象创建和延长生命周期。在实际优化时,可以组合使用:
- 用
slots压缩单个实例的内存占用。 - 用对象池或缓存复用实例,减少创建/销毁频率。
- 在缓存等场景使用弱引用,让不再使用的实例能被自动回收,防止内存泄漏。
这三种工具都是 Python 内存调优工具箱中的基础装备,掌握它们可以让你在遇到内存瓶颈时有清晰、低风险的优化路径。