雪花飘落

元组与列表的对比

列表与元组作为Python中非常常用的两种基本数据类型,经常被拿来比较,说到他们的区别,绕不开一点:列表是可变类型,元组是不可变类型,今天就从这里展开谈一谈。

列表和元组实际存的是什么

先来看一个例子:

import sys

ls = []
ls1 = [1]
ls2 = [1, "a"]
ls3 = [1, "a", "zhiyinnitaimei"]
# 结果为 56 64 72 80
print(sys.getsizeof(ls))
print(sys.getsizeof(ls1))
print(sys.getsizeof(ls2))
print(sys.getsizeof(ls3))

在这里我们可以看出,一个空列表占用的字节是56,然后每增加一个元素,占用的字节数始终是加8,无论新元素的类型与长度如何。

这里先说一嘴,为什么空列表也需要占用56个字节,因为Python中所有一切都是对象,其实是不存在所谓的“基本数据类型”,就拿int来说吧,一个int占用的字节是4个,在C语言或者Java中都是如此,但是在Python中,如果查看整数1的空间,却是28字节,Python的官方实现是C语言实现的,而Python中的int并不是对应C语言中的int,而是一个复杂的结构体,这也能解释为什么python中天然可以实现高精度运算。作为结构体,自然需要额外的空间来存储其它的信息。所以一个整数1,也需要占用28字节。因此,即使是一个空列表,系统也会分配56字节的空间。

 

再回到刚才的问题上来,当在列表中增加元素时,不管新增的元素类型如何长度如何,列表的空间始终增加8字节。8字节的空间,显然是不可能放下这些妖魔鬼怪的,所以答案很明显了,列表其实根本不是作为存储元素的“容器”,里面放的不是数据本身,而是指向数据地址的指针。而如果对元组做相同的实验,也是如此,元组中存放的依然是数据的指针。

元组真的“不可变”吗?

看一个例子:

tp = (1, [2, 3])
# 如果我们想把元组中的列表替换为一个新列表,会报错
# tp[1] = [4,5]
tp[1][0] = 4
# 但是我们可以修改列表中的元素,最后元组会变为(1, [4, 3])
print(tp)

这里元组的样子发生了改变,那是否可以说元组就是可变的呢?答案当然是否定的。前面我们说过,元组中存的是数据的地址,元组的不可变,指的是它存放的东西,也就是数据的地址是不能改变的,那至于这个地址里面发生了什么样的变化,它就管不着了。列表是可变的,所以如果我们去修改列表中的元素,列表的地址依然没变,但是当我们执行tp[1] = [4,5]的时候,是想要创建一个新的列表,然后把指针指向这个新列表,那地址当然就会发生改变,这个时候违背了元组不可变的规则,这就不行了。

 

所以,元组的不可变,指的是元组中数据的地址不能发生改变。

元组存在的必要性

元组能做的,列表都能做,元组不能做的,列表也能做。那元组能干啥呢?

其实就是三点:

(1)元组能保证数据安全,很多时候Python内置的函数或者方法都会要求我们传入元组类型的参数,或者是把数据打包成元组返回。因为元组不可变的特性,能够保证数据在处理或者传输的过程中的相对安全。比如你拿列表去排个序,执行list.sort()方法之后,序是排好了,但是原始的数据就没有了,而元组排序,只能使用sorted(),生成一个新的排好序的元组,并且原来的元组还在。
(2)空间更小,一个空列表是56字节,而一个空元组只占用40字节。因为Python中的数据类型本质都是以结构体的形式存在的,而列表要比元组多一个属性,那就是“增量”,列表有额外分配空间的需求,但元组没有。

(3)访问速度更快。

from timeit import timeit

code1 = '''
ls = [1,2,3]
ls[0]
'''

code2 = '''
tp = (1,2,3)
tp[0]
'''
# 大约是0.06s(依据机器性能和运行环境会有偏差)
print(timeit(code1))
# 大约是0.02s
print(timeit(code2))

使用timeit函数测试从创建到访问的速度,可以看出元组的效率约为列表的三倍。这是因为元组在程序中是以常量的形式存在的,对于常量,那么程序只需要构造一次就可以直接访问,而像列表这种可变类型的数据,每次访问的时候都需要重新构造,并且因为元组的结构信息比列表更简单,所有构建的时候也会更快。所以综合来说,元组的访问性能是要远强于列表的。

posted @ 2022-10-07 18:36  haruyuki  阅读(199)  评论(0编辑  收藏  举报