blog
blog copied to clipboard
Python元组和列表
一、元组和列表都属于基础数据结构中的“数组”
tuple 和 list 的内部实现都是 array(数组)的形式,而不是 linked list(链表)。
- list 因为可变,所以是一个 over-allocate 的 array;
- 由于列表是动态的,所以它需要存储指针,来指向对应的元素。
- 列表空间分配的过程:为了减小每次增加/删减操作时空间分配的开销,Python 每次分配空间时都会额外多分配一些,这样的机制(over-allocating)保证了其操作的高效性:增加 / 删除的时间复杂度均为 O(1) 。
- 对于元组,元组长度大小固定,元素不可变,所以存储空间固定。
- tuple 因为不可变,所以长度大小固定。
基础数据结构中的数组、链表
应用场景:
- 需要存储多个元素(多项数据)时,可使用数组或链表。
数组、链表的区别:
-
数组的元素都在一起,即在内存中都是相连的(紧靠在一起的)。 链表的元素是分开的,链表中的元素可存储在内存的任何地方,其中每个元素都存储了下一个元素的地址,从而使一系列随机的内存地址串在一起。
-
数组的元素带编号,元素的位置称为索引。 假设有一个数组
array = [1, 2, 3]
,第1个元素(索引为0的元素)是1(array[0] = 1
)。 -
数组的读取速度很快。 需要随机地读取元素时,数组的效率很高,数组支持随机访问,因为知道其中每个元素的地址,可迅速找到数组的任何元素。 在链表中,元素并非都靠在一起的,必须先读取第一个元素,根据其中的地址再读取第二个元素,以此类推。链表只能顺序访问,要读取链表的第十个元素,得先读取前九个元素,并沿链接找到第十个元素。随机访问意味着可直接跳到第十个元素。
-
链表的插入和删除速度很快。 需要在中间插入元素时,使用链表插入元素很简单,只需修改它前面的那个元素指向的地址;而使用数组时,则必须将后面的元素都向后移。 删除元素,链表也是更好的选择,因为只需修改前一个元素指向的地址即可;而使用数组时,删除元素后,必须将后面的元素都向前移。
常见的数组和列表操作的运行时间( O~(n)~ 为线性时间,O~(1)~ 为常量时间):
数组 | 链表 | |
---|---|---|
读取 | O(1) | O(n) |
插入(内存不足插入会失败) | O(n) | O(1) |
删除 | O(n) | O(1) |
二、元组和列表都是一个可以放置任意数据类型的有序集合
在绝大多数编程语言中,集合的数据类型必须一致。不过,对于 Python 的元组和列表来说,并无此要求:
tup = ('Alice', 18) # 元组中同时含有int和string类型的元素
tup
# Jupyter Notebook Out:
('Alice', 18)
l = [1, 2, 'hello', 'world'] # 列表中同时含有int和string类型的元素
l
# Jupyter Notebook Out:
[1, 2, 'hello', 'world']
三、元组是静态的,列表是动态的
- 元组是静态的,长度大小固定,无法增加删减或者改变(immutable)
- 列表是动态的,长度大小不固定,可以随意地增加、删减或者改变元素(mutable)
# 无法修改元组元素
tup = (1, 2, 3, 4)
tup[3] = 40
tup
# Jupyter Notebook Out:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-0151a4c9e088> in <module>
1 # 无法修改元组元素
2 tup = (1, 2, 3, 4)
----> 3 tup[3] = 40
4 tup
TypeError: 'tuple' object does not support item assignment
# 如果想对已有的元组做任何"改变",那就只能重新开辟一块内存,创建新的元组了。
tup = (1, 2, 3, 4)
new_tup = tup + (5, ) # 创建新的元组new_tup,并依次填充原元组的值
new_tup
# Jupyter Notebook Out:
(1, 2, 3, 4, 5)
# 修改列表元素
l = [1, 2, 3, 4]
l[3] = 40
l
# Jupyter Notebook Out:
[1, 2, 3, 40]
l = [1, 2, 3, 4]
l.append(5) # 添加元素5到原列表的末尾
l
# Jupyter Notebook Out:
[1, 2, 3, 4, 5]
四、元组和列表都支持负数索引
和其他语言不同,Python 中的元组和列表都支持负数索引,-1 表示最后一个元素,-2 表示倒数第二个元素,以此类推。
tup = (1, 2, 3, 4)
tup[-1]
# Jupyter Notebook Out:
4
l = [1, 2, 3, 4]
l[-2]
# Jupyter Notebook Out:
3
五、元组和列表都支持切片操作
tup = (1, 2, 3, 4)
tup[1:3] # 返回元组中索引从1到2的子元组
# Jupyter Notebook Out:
(2, 3)
l = [1, 2, 3, 4]
l[1:10] # 返回列表中索引从1到9的子列表
# Jupyter Notebook Out:
[2, 3, 4]
六、元组和列表都可以随意嵌套
# 元组的每一个元素也是一个元组
tup = ((1, 2, 3), (4, 5, 6))
# 列表的每一个元素也是一个列表
l = [[1, 2, 3], [4, 5]]
七、元组和列表可以通过list()
和tuple()
函数相互转换
tuple([1, 2, 3])
# Jupyter Notebook Out:
(1, 2, 3)
list((1, 2, 3))
# Jupyter Notebook Out:
[1, 2, 3]
八、元组和列表常用的内置函数
l = [3, 2, 3, 7, 8, 1]
l = [3, 2, 3, 7, 8, 1]
# count(item) 表示统计元组 / 列表中 item 出现的次数
l.count(3) # Jupyter Notebook Out: 2
# index(item) 表示返回元组 / 列表中 item 第一次出现的索引
l.index(7) # Jupyter Notebook Out: 3
# list.reverse() 和 list.sort() 分别表示原地倒转列表和排序(注意,元组没有内置的这两个函数)
l.reverse()
l # Jupyter Notebook Out: [1, 8, 7, 3, 2, 3]
l.sort()
l # Jupyter Notebook Out: [1, 2, 3, 3, 7, 8]
tup = (3, 2, 3, 7, 8, 1)
# reversed() 和 sorted() 同样表示对列表 / 元组进行倒转和排序:
# reversed() 返回一个倒转后的迭代器(这里使用 list() 函数再将其转换为列表);
# sorted() 返回排好序的新列表。
list(reversed(tup)) # Jupyter Notebook Out: [1, 8, 7, 3, 2, 3]
sorted(tup) # Jupyter Notebook Out: [1, 2, 3, 3, 7, 8]
九、元组和列表存储方式的差异
- 元组和列表最重要的区别就是,列表是动态的、可变的,而元组是静态的、不可变的。
- 储存相同的元素,元组的存储空间比列表要少。
tup = (1, 2, 3)
tup.__sizeof__()
# Jupyter Notebook Out:
48
l = [1, 2, 3]
l.__sizeof__()
# Jupyter Notebook Out:
64
- 由于列表是动态的,所以它需要存储指针,来指向对应的元素。
- 列表空间分配的过程:为了减小每次增加/删减操作时空间分配的开销,Python 每次分配空间时都会额外多分配一些,这样的机制(over-allocating)保证了其操作的高效性:增加 / 删除的时间复杂度均为 O(1) 。
- 对于元组,元组长度大小固定,元素不可变,所以存储空间固定。
l = []
# 空列表的存储空间为40字节
l.__sizeof__() # Jupyter Notebook Out: 40
# 加入了元素1之后,列表为其分配了可以存储4个元素的空间 (72 - 40)/8 = 4
l.append(1)
l.__sizeof__() # Jupyter Notebook Out: 72
# 由于之前分配了空间,所以加入元素2,列表空间不变
l.append(2)
l.__sizeof__() # Jupyter Notebook Out: 72
# 由于之前分配了空间,所以加入元素3,列表空间不变
l.append(3)
l.__sizeof__() # Jupyter Notebook Out: 72
# 由于之前分配了空间,所以加入元素4,列表空间不变
l.append(4)
l.__sizeof__() # Jupyter Notebook Out: 72
# 加入元素5之后,列表的空间不足,所以又额外分配了可以存储4个元素的空间
l.append(5)
l.__sizeof__() # Jupyter Notebook Out: 104
根据以上分析元组和列表存储方式的差异,元组和列表储存空间大小差异似乎不大。 但是想象一下,如果列表和元组存储元素的个数是一亿,十亿甚至更大数量级时,还能忽略这样的差异吗?
十、元组和列表的性能
- 元组要比列表更加轻量级一些,元组的性能速度要略优于列表。
- Python 会在后台,对静态数据做一些资源缓存(resource caching)。 通常来说,因为垃圾回收机制的存在,如果一些变量不被使用了,Python 就会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。 但是对于一些静态变量,比如元组,如果它不被使用并且占用空间不大时,Python 会暂时缓存这部分内存。 这样,下次我们再创建同样大小的元组时,Python 就可以不用再向操作系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。
- 如果想要增加、删减或者改变元素,那么列表显然更优。因为对于元组,需要通过新建一个元组来完成。
初始化一个相同元素的元组和列表分别所需的时间:元组的初始化速度,要比列表快5倍(此处使用macOS):
$ python3 -m timeit 'x=(1,2,3,4,5,6)'
20000000 loops, best of 5: 19 nsec per loop
python3 -m timeit 'x=[1,2,3,4,5,6]'
5000000 loops, best of 5: 90.3 nsec per loop
但如果是索引操作的话,两者的速度差别非常小,几乎可以忽略不计:
$ python3 -m timeit -s 'x=(1,2,3,4,5,6)' 'y=x[3]'
5000000 loops, best of 5: 43 nsec per loop
$ python3 -m timeit -s 'x=[1,2,3,4,5,6]' 'y=x[3]'
5000000 loops, best of 5: 42.1 nsec per loop
十一、元组和列表的使用场景
# 如果存储的数据和数量不变,比如有一个函数,需要返回的是一个地点的经纬度,然后直接传给前端渲染,那么肯定选用元组更合适:
def get_location():
......
return (longitude, latitude)
# 如果存储的数据或数量是可变的,比如社交平台上的一个日志功能,是统计一个用户在一周之内看了哪些用户的帖子,那么则用列表更合适:
viewer_owner_id_list = [] # 里面的每个元素记录了这个viewer一周内看过的所有owner的id
records = queryDB(viewer_id) # 索引数据库,拿到某个viewer一周内的日志
for record in records:
viewer_owner_id_list.append(record.id)