对于Python来说,列表可谓是用到的非常多的数据结构之一了,但是Python还有另外一个数据结构叫做元组,直观表现来说,元组就像是不可变的列表,那么问题来了,元组和列表的区别是什么呢?什么时候应该用元组,什么时候应该用列表呢?我在刚开始学习Python的过程当中一直有这种困惑,本文是我总结的一些关于Python列表和元组的相关知识,下面来一起看一下吧。
基础知识
总的来说,列表和元组实际上都是一个可以放置任意数据类型的有序数组, 相比于其他语言,比如说C或者Java等等,他们数组中的元素类型必须保持一致,而Python当中并不存在这个限制,可以在里面放任意的元素。
l = [1, '1', True, None, []] # 列表中同时包含数字, 字符串, 布尔值, None, 和空列表
tup = (1, '1', True, None, ()) # 元组中同时包含数字, 字符串, 布尔值, None, 和空元组
从表现形式上看来,数组用的是中括号([]
)而元组用的是小括号(()
),他们的区别如下:
- 列表是可变的(mutable), 长度大小不固定,可以任意的增删改元素
- 元组是不可变的(immutable), 长度大小固定, 无法对元素删减或者改变
我们通过一个具体的样例来看一下上面的区别,对于列表,我们很容易的利用索引修改当中的元素,而对于元组来说,如果对它进行修改操作,则会触发错误,原因就是元组是不可变的。
l = [1, 2, 3]
l[0] = -1
print(l)
tup = (1, 2, 3)
tup[0] = -1
我们利用colab来演示结果:
那么问题来了,元组一旦被创建就无法对它进行做任何的改变, 那么如果我们想改变元组当中的值,又该如何操作呢?那只能重新开辟一块内存,创建新的元组了, 同样我们来看下面的一个例子:
l = [1, 2, 3]
print(f'old list addr: {id(l)}')
l += [4]
print(f'new list addr: {id(l)}')
tup = (1, 2, 3)
print(f'old tuple addr: {id(tup)}')
tup += (4, )
print(f'new tuple addr: {id(tup)}')
可以发现,对于列表进行增加元素的操作,列表本身的内存地址并没有发生改变,而对于元组来说,它本身的内存地址发生了改变,也就符合上面所说的,如果想要对元组中的元素进行新增操作,则会重新开辟一块内存。
基本操作
下面来简单介绍一下列表和元组中的一些常用的基本操作。
索引
对于Python的列表和元组来说,有一个非常强大的功能就是支持负数索引,其中-1
表示最后一个元素, -2
表示倒数第二个元素,以此类推。
l = [1, 2, 3]
print(f'last element in l is {l[-1]}')
tup = (1, 2, 3)
print(f'last element in tup is {tup[-1]}')
切片
除了索引,Python当中的元组和列表还支持切片操作,对于这个我不打算在这里面详细去说,仅介绍简单的基础用法,如有需求,请参考官方文档。
l = [1, 2, 3, 4]
print(f'sub element in l is {l[1:3]}')
tup = (1, 2, 3, 4)
print(f'sub element in tup is {tup[1:3]}')
相互转换
对于列表和元组,可以使用tuple()
和list()
来进行相互的转换。
var = [1, 2, 3, 4]
print(f'type of var is {type(var)}')
var = tuple(var)
print(f'type of var is {type(var)}')
var = list(var)
print(f'type of var is {type(var)}')
内置函数
下面,我们来看一下列表和元组中的内置函数,这里我只介绍一些比较常用的,对于其他的可以参考官方文档。
count
表示统计列表/元组中元素出现的个数
index
返回列表/元组元素第一次出现的索引
sort
& reverse
这两个函数分别对列表的元素进行正序排列和逆序排列(元组是没有这两个函数的, 原因是他会改变列表本身)
sorted
& reversed
这两个函数是对于列表和元组进行排序的函数,reversed
返回的是迭代器, 而sorted
返回的是列表。
下面我们来看一下具体的样例:
l = [3, 3, 4, 5, 5, 4, 3, 2, 1, 1, 2, 3, 3, 2, 2]
tup = (3, 3, 4, 5, 5, 4, 3, 2, 1, 1, 2, 3, 3, 2, 2)
# count
print(f'count of 3 in l is {l.count(3)}')
print(f'count of 3 in tup is {tup.count(3)}')
# index
print(f'index of 5 in l is {l.index(5)}')
print(f'index of 5 in tup is {tup.index(5)}')
# sort & reverse
l.sort()
print(f'sorted l is {l}')
l.reverse()
print(f'reversed l is {l}')
# sorted & reversed
print(f'sorted l is {sorted(l)}')
print(f'reversed l is {reversed(l)}')
print(f'sorted tup is {sorted(tup)}')
print(f'reversed tup is {reversed(tup)}')
列表和元组的存储方式
由于列表和元组的特性,列表是可变的,而元组是不可变的,这种差异必然是由于底层的存储方式不同而决定的,首先我们来看下面的样例。
l = [1, ]
print(f'size of l is {l.__sizeof__()}')
tup = (1, )
print(f'size of tup is {tup.__sizeof__()}')
我们可以看到,元组相对于列表来说,在放置相同元素的情况下,元组所占用的空间比列表少了16个字节,主要原因是由于列表是动态的,因此对于列表的存储需要指针,用来指向对应的元素,这里我们存放的是int
类型,占用了8个字节,其次由于列表的可变性,需要额外的存储来保存已经分配的长度大小的信息(8个字节),这样才能在空闲长度不足的情况下及时的分配内存开辟空间,我们来看下面的一个例子:
l = []
print(f'initial size of l is {l.__sizeof__()}')
for i in range(5):
l.append(i)
print(f'size of l after append {i} is {l.__sizeof__()}')
我们可以发现,Python为了减少每次列表扩容导致的开销,每次会多分配一定的内存,这样的机制保证了其操作的高效性。而对于元组来说,它本身是不可变的,因此存储空间固定。
列表和元组的性能分析
通过上文对于存储差异的分析,我们可以认为,元组要比列表要更加的轻量一些,因此,总的来说,元组的性能要稍好于列表。
我们来看下面一个列子
python -m timeit 'x=(1,2,3,4,5,6)'
>>> 100000000 loops, best of 3: 0.0106 usec per loop
python -m timeit 'x=[1,2,3,4,5,6]'
>>> 10000000 loops, best of 3: 0.0553 usec per loop
这段代码是在我的电脑上运行的,不同的电脑可能会有差异,但是我们可以看出,对于元组的初始化操作要比列表快5倍左右,原因主要是因为Python会对于一些静态数据做资源缓存, 对于元组来说,当它占用的空间不大的时候,Python会暂时缓存这一部分内存,当下次请求同样大小的元组的时候,Python无需向系统再次申请这一部分内存,从而提高了程序的运行效率。
当然,如果对于增删操作来说,列表是优于元组的,原因显而易见,元组是需要重新开辟内存,而列表缺不需要这样。
列表和元组的使用场景
上面分析了这么多,那么什么时候应该用元组,什么时候应该用列表呢?这里当然需要具体情况具体分析。
- 如果存储的数据和数量不变,比如保存图片中所有像素点的rgb值,这个显然是利用元组比较合适一些
- 如果数据的数量可变,比如需要统计或者计算出来的结果保存,那么显然是利用列表更加合适一些
总结
对于Python当中的元组和列表,从表现形式来看,仅仅只有列表是可变的,而元组是不可变的,但是这个表面特性是由于内部实现而决定的,因此也就造成了对于某些操作的性能差异,因此在选择数据结构的过程中,应该去考虑这一点,选择更加合适的数据结构,总的来说,列表和元组的区别可以总结为如下两点:
- 列表是可变的,可以任意增加或者删除元素,因此存储空间要略大于元组
- 元组是不可变的,无法对元素进行增加或者删除,更加轻量,适合保存不变的数据
文章最后,来看一个例子,下面的程序运行结果是怎么样的呢?
t = (1, 2, [30, 40])
t[2] += [50, 60]
- t变成
(1, 2, [20, 30, 40, 50])
- 因为tuple不支持对他的元素赋值,因此会触发TypeError异常
- 两个都是错的
- 两个都是对的
参考资料
- Fluent Python By Luciano Ramalho
- Python 官方文档
- Lists vs Tuples in Python
- Optimization tricks in Python: lists and tuples | Artem Golubin