Python容器内存三要素
一、随机访问与连续内存
随机访问
在Python中,随机访问指的是通过索引直接获取容器中任意位置元素的能力,这个操作的时间是恒定的,与容器的大小无关。
支持随机访问的对象包括list、str、tuple、bytes、bytearray。例如my_list[1000]和my_list[0]的访问速度几乎一样快。
不支持高效随机访问的对象包括collections.deque、set、dict。deque虽然可以用下标访问,但它的实现是双向链表,访问中间元素需要遍历,效率是O(n)。
set和dict不是序列,不支持下标索引,它们基于哈希表实现,用于快速判断成员关系和按键取值,这不是随机访问。当使用中括号运算符通过数字索引从list、str、tuple中取元素时,你就在利用它们的随机访问特性。
连续内存
连续内存指的是对象将其数据元素一个接一个地存储在一块完整的内存区域中。
在连续内存中存储数据的Python对象包括str、tuple、bytes。这些不可变类型将其所有内容(对于str是Unicode码点,对于bytes是字节)存储在连续的内存块中。
list是一个特殊情况,它本身并不将所有对象连续存储,而是存储指向各个Python对象的引用,这些引用本身存储在连续内存中。这就是为什么list支持随机访问的原因,通过索引i找到对应的引用,只需计算起始地址加i乘以引用大小。
list的连续性体现在引用数组上,而str和tuple的连续性体现在它们的数据本身。
二、静态内存与动态内存
动态内存
动态内存是指程序在运行时,根据实际需要向系统申请并分配的内存空间。内存的分配和释放在程序执行期间动态进行,大小和数量都可以灵活变化。
Python的动态内存管理包括以下两个关键机制:
引用计数机制:每个对象都有一个引用计数器,当引用数为零时,该对象的内存会立即被释放。
垃圾回收机制:用于检测和清理循环引用的对象,例如列表或字典之间互相引用的情况。
Python的内存模型主要依靠动态内存。所有在代码中创建的对象,例如list、dict、MyClass实例,都是在运行时由解释器从堆内存中动态申请的。
对开发者来说,这意味着创建对象时无需考虑分配位置,也无需手动释放内存,Python会自动完成这些操作。
静态内存
静态内存是指在程序运行前就已经确定并分配好的内存区域。这部分内存的大小和生命周期在程序编译或加载时就固定下来,在程序整个运行过程中不会改变。
在Python中几乎不存在静态内存分配的概念。
可以认为解释器在启动时会预先创建一些小整数(如-5到256)和短字符串,这些对象在解释器的整个生命周期中都存在,类似静态分配。但对于普通开发者来说,无法直接进行静态分配。所有变量绑定的对象都在堆上动态分配。
三、物理大小与逻辑大小
逻辑大小
逻辑大小是程序员看到和操作的容器尺寸,即容器中实际包含的元素个数。
可以通过len函数获取。
例如my_list = ['a', 'b', 'c']的逻辑大小是3。
物理大小
物理大小是解释器为了优化性能而维护的内部状态。
它表示list底层引用数组实际占用的内存空间所能容纳的元素个数,通常大于逻辑大小。
这样做是为了支持高效的append操作。
如果每次append都要重新分配内存并复制所有元素,性能会变差。
通过预留额外空间,也就是超额分配,大多数append操作只需将引用放入空闲位置即可完成。
可以通过sys.getsizeof函数查看列表对象占用的字节数。
当在append操作后看到这个值突然增大,就说明Python进行了内存扩容。