前言
面试中,会被问到一个题目是,List与Tuple的不同,以及为什么需要两者。今天就把这个知识点整理一下。
List与Tuple基础
List和Tuple,即列表和元组,都是一个可以放置任意数据类型的有序集合。不同的是,列表是可变的(mutable),元组是不可变的(immutable)(关于两者差异,我们放到下一节详细说明)。
- 列表和元组都支持负数索引
l = [1,2,3,4]
1[-1]
4
t = (1,2,3,4)
t[-1]
4
- 除了基本的初始化、索引之外,列表和元组都支持切片操作
l = [1,2,3,4]
l[1:3]
[2, 3]
t = (1,2,3,4)
t[1:3]
(2, 3)
- 列表和元组均可任意嵌套,可以通过list()和tuple()函数相互转换
l = [[1, 2, 3], [4, 5]] # 列表的每一个元素也是一个列表
t = ((1, 2, 3), (4, 5, 6)) # 元组的每一个元素也是一个元组
list((1,2,3))
[1,2,3]
tuple([1,2,3])
(1,2,3)
- 常用的内置函数基本相同
l = [3, 2, 3, 7, 8, 1]
l.count(3)
2
l.index(7)
3
l.reverse()
l
[1, 8, 7, 3, 2, 3]
l.sort()
l
[1, 2, 3, 3, 7, 8]
tup = (3, 2, 3, 7, 8, 1)
tup.count(3)
2
tup.index(7)
3
list(reversed(tup))
[1, 8, 7, 3, 2, 3]
sorted(tup)
[1, 2, 3, 3, 7, 8]
注意,list.reverse() 和 list.sort() 分别表示原地倒转列表和排序,但元组没有内置的这两个函数)。reversed() 和 sorted() 同样表示对列表 / 元组进行倒转和排序,但是会返回一个倒转后或者排好序的新的列表 / 元组。
List与Tuple的不同
上一小节,我们简单回顾了一下列表和元组的基础,这一小节,我们来来细品列表和元组的不同。
一言以蔽之,核心不同即,列表是可变对象(mutable),而元组是不可变对象(immutable)。正因为如此,才导致了两者从三个方面有些差异。
1.性能方面
通常来说,因为有垃圾回收机制的存在,如果一些变量不被使用了,Python会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。但对于一些不可变对象,比如元组,如果它不被使用并且占用空间不大时,Python会暂时缓存这部分内容,这样下次再创建同样大小的元组时,Python就可以不需要向操作系统发出请求,寻找内存,而是直接分配之前缓存的内容空间。这样能大大加快程序的运行速度。
我们可以用python3 -m timeit -s '<operation>'来观察一下具体操作的耗时情况。
初始化性能
python3 -m timeit 'a=[1,2,3,4,5,6,7,8,9,10]'
2000000 loops, best of 5: 107 nsec per loop
python3 -m timeit 'b=(1,2,3,4,5,6,7,8,9,10)'
20000000 loops, best of 5: 17.5 nsec per loop
可以看到,元组初始化耗时,大概是列表的6倍。在初始化方面,元组性能胜。
索引操作性能
python3 -m timeit -s 'a=[1,2,3,4,5,6,7,8,9,10]' -s 'a[3]'
20000000 loops, best of 5: 11.4 nsec per loop
python3 -m timeit -s 'b=(1,2,3,4,5,6,7,8,9,10)' -s 'b[3]'
20000000 loops, best of 5: 11.4 nsec per loop
差别不大,平手。
追加修改的性能
python3 -m timeit \
-s "L = []" \
-s "x = range(10000)" \
"for item in x:" " L.append(item)"
200 loops, best of 5: 1.14 msec per loop
python3 -m timeit \
-s "T = ()" \
-s "x = range(10000)" \
"for item in x:" " T += (item,)"
2 loops, best of 5: 400 msec per loop
可以看到,在追加时,如果我们用元组,耗时大概是列表追加的350倍。因为元组是不可变对象,每次追加,都是新创建一个元组。
2.内存效率
首先,我们看一个例子:
l = [1,2,3]
l.__sizeof__()
64
t = (1,2,3)
t.__sizeof__()
48
对于列表和元组,虽然初始化时放了相同的元素,但是元组的存储空间,比列表要少16个字节。这是因为,由于列表是可变对象,需要存储指针,来指向对应的元素(上述例子中,int型,8字节)。另外,由于列表可变,需要额外存储已经分配的长度大小(8字节),这样才可以实时追踪列表空间的使用情况,当空间不足时,及时分配额外空间。
l = []
l.__sizeof__() // 空列表的存储空间为40字节
40
l.append(1)
l.__sizeof__()
72 // 加入了元素1之后,列表为其分配了可以存储4个元素的空间 (72 - 40)/8 = 4
l.append(2)
l.__sizeof__()
72 // 由于之前分配了空间,所以加入元素2,列表空间不变
l.append(3)
l.__sizeof__()
72 // 同上
l.append(4)
l.__sizeof__()
72 // 同上
l.append(5)
l.__sizeof__()
104 // 加入元素5之后,列表的空间不足,所以又额外分配了可以存储4个元素的空间
从上面的例子中,我们可以看到列表空间分配的大概过程。为了减少每次增加、删除操作时空间分配的开销,Python每次分配空间时会额外多分配一些。这样的机制(over-allocating)保证了其操作的高效性;增加、删除操作的时间复杂度为O(1)。
但由于元组是不可变的,长度大小固定,故存储空间固定。那么,我们可以得到一个初步结论,对于不可变对象来说,有这样一个好处,在于内存更有效,占用更小。
此外,对于不可变对象,比如String,在Python3.6之前,是有一个String Interning的概念的,即如下所示:
c = "hi"
d = "hi"
id(c)
4561935656
id(d)
4561935656
在CPython中,如果创建几个不可变对象,但都具有相同的值,Python可以将这几个对象指向一个。这种优化在3.6之后就取消了,但是不妨碍这种操作更节约内存、节约时间。所以,在内存效率层面,不可变对象,元组更胜一筹。
3.工程层面
在项目中,使用元组或者列表,还要给予工程角度去考量,比如返回一个地理位置的坐标,一定是元组更合适——不可变对象,值固定。在某些场景下,使用元祖,还可以避免debug过程中值变更带来的一些失误。这就是仁者见仁智者见智的事情了。
Reference
[1].Python: What is the Difference between a List and a Tuple?