性能提升5倍!Python列表和元组的底层原理揭秘
这是一份有基础、有深度的 Python 必备秘籍。
基础
python
################## 初始化
>>> l = [1, 2, "hello"]
>>> l
[1, 2, 'hello']
>>> t = (1, 2, "hello")
>>> t
(1, 2, 'hello')
################## 负数索引
>>> l[-1]
'hello'
>>> t[-1]
'hello'
################## 切片
>>> t[1:]
(2, 'hello')
>>> l[1:]
[2, 'hello']
################## 嵌套
>>> l = [[1,2], ["hello"]]
>>> l
[[1, 2], ['hello']]
>>> t = ((1,2), ("hello"))
>>> t
((1, 2), 'hello')
################## 转换
>>> tuple(l)
([1, 2], ['hello'])
>>> list(t)
[(1, 2), 'hello']
################## 列表内置函数
>>> l = [3,2,3,7,8,1]
>>> l.count(3) # 统计 3 的个数
2
>>> l.index(7) # 查看 7 所在的索引位置
3
>>> l.reverse() # 原地倒置
>>> l
[1, 8, 7, 3, 2, 3]
>>> l.sort() # 原地排序
>>> l
[1, 2, 3, 3, 7, 8]
################## 元组内置函数
>>> t = (3,2,3,7,8,1)
>>> t.count(3) # 统计 3 的个数
2
>>> t.index(7) # 查看 7 所在的索引位置
3
>>> list(reversed(t)) # 原地倒置,并以列表形式输出
[1, 8, 7, 3, 2, 3]
>>> sorted(t) # 排序输出
[1, 2, 3, 3, 7, 8]
操作差异
python
# 列表是动态的,长度大小不固定,可以随意增、删、改
# 元组是静态的,长度大小固定,无法增、删、改
>>> l[0] = 3
>>> l
[3, 2, 'hello']
>>> t[0] = 3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
存储差异
python
################## 初始化
l = []
l.__sizeof__()
40
>>> l = [1,2,3] # 一个 int 占 8 个字节
>>> l.__sizeof__()
64
################## 空间分配
# 结论:
# Python 每次分配空间时,都会额外多分配一些。
# 这样,可以减少每次增加/删减操作时,空间分配的开销,保证了操作的高效性。
# 存储空间,比元组要大
>>> l = []
>>> l.__sizeof__()
40
>>> l.append(1)
>>> l.__sizeof__()
72 # 列表每次分配 4 个元素的空间 (72-40)/8=4
>>> l.append(2)
>>> l.__sizeof__()
72 # 可以存得下
>>> l.append(3)
>>> l.__sizeof__()
72 # 同上
>>> l.append(4)
>>> l.__sizeof__()
72 # 同上
>>> l.append(5)
>>> l.__sizeof__()
104 # 列表空间不足,扩容。再给分配 4 个元素的空间
################## 元组
# 结论:
# 大小固定,元素不可变。故存储空间固定不变
# 存储空间比列表小。
>>> t = (1,2,3)
>>> t.__sizeof__()
48
为什么元组的存储空间比列表少 16 字节?
markdown
1. 列表是动态的,它需要指针,来指向对应的元素
2. 列表是可变的,所以,需要额外存储已经分配的长度大小(8 字节),这样才能实时追踪列表空间的使用情况,及时分配额外空间。
性能差异
结论:元组的性能略优于列表。
1、初始化时间对比
bash
# 结论:元组比列表快约 5 倍
$ python3.7 -m timeit "x=(1,2,3,4,5)"
20000000 loops, best of 5: 16.9 nsec per loop
$ python3.7 -m timeit "x=[1,2,3,4,5]"
5000000 loops, best of 5: 82.3 nsec per loop
2、索引操作对比
bash
# 结论:元组比列表快约 2.5 倍
$ python3.7 -m timeit "x=(1,2,3,4,5)" "y=x[3]"
5000000 loops, best of 5: 45.6 nsec per loop
$ python3.7 -m timeit "x=[1,2,3,4,5]" "y=x[3]"
2000000 loops, best of 5: 113 nsec per loop
列表 和 元组 的内部实现
1、列表的底层结构
- 列表本质是过度分配的数组(over-allocate array)。
markdown
over-allocate 是什么?
就是当底层数组容量满了,而需要扩充的时候,python 依据规则会扩充多个位置出来。
即,扩充容量的时候,会多分配一些存储空间。
另外,当列表存储的元素在变少时,python 也会及时收缩底层的数组,避免造成内存浪费。
优点:提高了执行效率。
查看内存变化:
1. __sizeof__()
2. sys.getsizeof()
- 核心结构
scss
ob_item:指针列表,存储元素的内存地址。
allocated:预分配的总空间(通常大于实际元素数量 len(list) )。
ob_size:实际元素数量(即 len(list) 的值 )。

- 特点
scss
预分配空间(allocated)>= len(list) = 实际元素数(ob_size)
扩容时,按如下规律,申请更大空间:
0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
- Python 3.7 的 list 源码地址
ruby
listobject.h:
https://github.com/python/cpython/blob/949fe976d5c62ae63ed505ecf729f815d0baccfc/Include/listobject.h#L23
listobject.c:
https://github.com/python/cpython/blob/3d75bd15ac82575967db367c517d7e6e703a6de3/Objects/listobject.c#L33
2、元组的底层优化
-
元组本质也是数组,但空间大小固定,且做了特殊优化:
- 大小 ≤20 时,复用内部
free list缓存。创建相同元组时直接从缓存取,提升效率。 - 相比列表,元组不可变,无需考虑扩容,内存更紧凑。
- 大小 ≤20 时,复用内部
-
Python 3.7 的 tuple 源码地址
ruby
tupleobject.h:
https://github.com/python/cpython/blob/3d75bd15ac82575967db367c517d7e6e703a6de3/Include/tupleobject.h#L25
tupleobject.c:
https://github.com/python/cpython/blob/3d75bd15ac82575967db367c517d7e6e703a6de3/Objects/tupleobject.c#L16
使用场景
如果,存储数据和数量不变,用元组。
如果,存储的数据或数量是可变的,用列表
总结
- 列表是动态的,长度可变,可以随意增加、删减、更新元素。列表的存储空间略大于元组,性能图逊于元组。
- 元组是静态的,长度大小固定,不可以对元素进行增加、删减、更新操作。元组相对于列表更加轻量级,性能稍优。