Python内存分配机制深度解析:从空对象到动态扩容的隐藏细节

季宁
季宁
发布时间:2026-10-13 | 5人浏览过
原创

本文深入探讨Python内置对象的内存分配机制。通过sys.getsizeof()分析发现,空容器并非零占用,而是预分配了骨架与初始空间。文章揭示了可变对象在动态扩容时的超额与非均匀分配策略,对比静态创建与动态扩容下列表内存占用的差异,并指出元素缩减后内存不释放的“胖子无法减重”现象,以及清空后字典内存显著缩小的特殊行为。

Python内存分配机制深度解析:从空对象到动态扩容的隐藏细节

前言

Python中的sys模块是基础且关键的组件,它提供了与解释器交互的变量及函数,供解释器内部使用或维护。

本文将频繁调用该模块的getsizeof()方法,因此首先对其功能进行简要说明:

  • 该方法用于返回对象所占用的字节数(bytes)。
  • 需要注意的是,它仅计算对象直接占用的内存,不包含其引用的其他对象的内存大小。

以下是一个直观示例:


import sys

a = [1, 2]
b = [a, a] # 即 [[1, 2], [1, 2]]

# a、b 都只有两个元素,所以直接占用的大小相等
sys.getsizeof(a) # 结果:80
sys.getsizeof(b) # 结果:80

上述代码表明,一个仅包含两个元素的静态列表,其自身内存占用固定为80字节,无论其元素指向的具体对象是什么。

掌握了这一测量工具,我们开始探究Python内置对象在内存分配中隐藏的细节。

1、空对象并非“空”

1、空对象并非“空”

对于常见的空对象,如空字符串、空列表、空字典等,我们是否曾好奇它们是否占用内存?若占用,具体大小是多少?分配依据为何?

直接通过代码查看几类基本数据结构空对象的大小:


import sys
sys.getsizeof("")  # 49
sys.getsizeof([])  # 64
sys.getsizeof(())  # 48
sys.getsizeof(set()) # 224
sys.getsizeof(dict()) # 240

# 作为参照:
sys.getsizeof(1)  # 28
sys.getsizeof(True) # 28

结果显示,尽管都是空对象,但它们在内存分配上并非为零,且初始分配量较大(请牢记这些数值,后续会有对比)。

按大小排序:基础数字 < 空元组 < 空字符串 < 空列表 < 空集合 < 空字典。

这一现象如何解释?

因为所有空对象均为容器,可抽象理解为:部分内存用于构建容器骨架及记录元数据(如引用计数、使用量等),另一部分则为预分配空间。

2、内存扩充并非均匀进行

2、内存扩充并非均匀进行

空对象之所以不“空”,部分原因在于Python解释器为其预分配了初始空间。在初始容量范围内新增元素时,直接复用已有内存,从而避免频繁申请新内存。

那么,当初始内存耗尽后,新内存如何分配?


import sys
letters = "abcdefghijklmnopqrstuvwxyz"

a = []
for i in letters:
 a.append(i)
 print(f'{len(a)}, sys.getsizeof(a) = {sys.getsizeof(a)}')
 
b = set()
for j in letters:
 b.add(j)
 print(f'{len(b)}, sys.getsizeof(b) = {sys.getsizeof(b)}')

c = dict()
for k in letters:
 c[k] = k
 print(f'{len(c)}, sys.getsizeof(c) = {sys.getsizeof(c)}')

分别向三类可变对象添加26个元素,观察结果:

由此可看出可变对象在扩充时的机制:

  • 超额分配机制:申请新内存时并非按需精确分配,而是多分配一部分,以便在后续添加少量元素时无需立即再次申请内存。
  • 非均匀分配机制:三类对象申请新内存的频率不同,且同一类对象每次超额分配的内存增量并非固定,而是逐渐增大。

3、静态列表与动态列表不等价

3、静态列表与动态列表不等价

上述可变对象在扩充时具有相似的分配机制,动态扩容效果明显。

那么,静态创建的对象是否也遵循此机制?与动态扩容相比是否有区别?

先观察集合与字典:


# 静态创建对象
set_1 = {1, 2, 3, 4}
set_2 = {1, 2, 3, 4, 5}
dict_1 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5}
dict_2 = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6}

sys.getsizeof(set_1) # 224
sys.getsizeof(set_2) # 736
sys.getsizeof(dict_1) # 240
sys.getsizeof(dict_2) # 368

对比上一节截图可知,在元素数量相同时,静态创建的集合或字典所占内存与动态扩容时完全一致。

这一结论是否适用于列表?继续验证:


list_1 = ['a', 'b']
list_2 = ['a', 'b', 'c']
list_3 = ['a', 'b', 'c', 'd']
list_4 = ['a', 'b', 'c', 'd', 'e']

sys.getsizeof(list_1) # 80
sys.getsizeof(list_2) # 88
sys.getsizeof(list_3) # 96
sys.getsizeof(list_4) # 104

上一节截图显示,列表在前4个元素时占用96字节,5个元素时占用128字节,与此处结果明显矛盾。

因此,结论显而易见:在元素数量相等时,静态创建的列表所占内存可能小于动态扩容时的内存!

这意味着,看似相同的两种列表,实际内存结构不同!列表不等于列表。

4、缩减元素不会释放内存

4、缩减元素不会释放内存

前文提到,扩充可变对象时可能会申请新内存。

反之,若缩减可变对象并移除部分元素,新申请的内存是否会自动回收?


import sys
a = [1, 2, 3, 4]
sys.getsizeof(a) # 初始值:96
a.append(5)  # 扩充后:[1, 2, 3, 4, 5]
sys.getsizeof(a) # 扩充后:128
a.pop()   # 缩减后:[1, 2, 3, 4]
sys.getsizeof(a) # 缩减后:128

如代码所示,列表经历一次扩充和一次缩减后,虽然元素恢复原状,但所占内存空间并未自动释放。其他可变对象同理。

这就是Python的另一个小秘密:“胖子无法减重原理”——瘦子变胖容易,减肥也容易,但体重一旦增加便难以自动减少,哈哈~~~

5、空字典不等于初始空字典

5、空字典不等于初始空字典

使用pop()方法仅减少可变对象中的元素,不会释放已申请的内存空间。

此外,还有clear()方法,用于清空可变对象的所有元素,让我们测试一下:


import sys
a = [1, 2, 3]
b = {1, 2, 3}
c = {'a':1, 'b':2, 'c':3}

sys.getsizeof(a) # 88
sys.getsizeof(b) # 224
sys.getsizeof(c) # 240

a.clear()  # 清空后:[]
b.clear()  # 清空后:set()
c.clear()  # 清空后:{},也即 dict()

调用clear()方法后,我们得到几个空对象。

在第一小节中,这些对象的内存大小已被验证(前文提及会考查,请默写并回顾)。

然而,若此时再次验证,你会惊讶地发现,这些空对象的大小与前文记录并不完全一致!


# 承接前面的清空操作:
sys.getsizeof(a) # 64
sys.getsizeof(b) # 224
sys.getsizeof(c) # 72

空列表与空元组的大小保持不变,但空字典(72字节)竟比初始空字典(240字节)小得多!

也就是说,列表与元组在清空元素后回到起点,初心不变;而字典则是“赔了夫人又折兵”,不仅吐出了所有元素,还大幅减少了底层内存占用。

字典的这一秘密藏得较深,说实话我也是刚刚发现,至今百思不得其解……

以上就是Python在内存分配时的几个小秘密,阅读后你是否感到涨知识了呢?

你理解了几个?又产生了多少新疑问?欢迎留言交流。

对于那些尚未充分解释的小秘密,未来我们将逐步揭秘……

总结

以上就是本文的全部内容,希望本文的内容对大家的学习或工作具有一定的参考学习价值,谢谢大家对本站的支持。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表