在绝大多数编程语言中,集合的数据类型必须一致。不过,对于 Python 的列表和元组来说,并无此要求。实际上,列表和元组,都是一个可以放置任意数据类型的线性表。它们的区别是:

  • 列表是动态的,长度大小不固定,可以随意地增加、删减或者改变元素(mutable)。
  • 元组是静态的,长度大小固定,无法增加删减或者改变(immutable)。

如果想对已有的元组做任何的改变,那就是只能重新开辟一块内存,创建新的元组,然后把原来两个元组的值依次填充进去。而对于列表来说,由于其是动态的,我们只需简单地在列表末尾,加入对应元素就可以了。

列表和元组的共同点:

  1. Python 中的列表和元组都支持负数索引,-1 表示最后一个元素,-2 表示倒数第二个元素,以此类推。
l = [1, 2, 3, 4]
l[-1]

#输出
4

t = (1, 2, 3, 4)
t[-1]

#输出
4
  1. 列表和元组都支持切片操作
l = [1, 2, 3, 4]
#注意:与函数range()类似,Python在到达指定的第二个索引前面的元素后停止。
l[1:3] # 返回列表中索引从1到2的子列表

#输出
[2, 3]

tup = (1, 2, 3, 4)
tup[1:3] # 返回元组中索引从1到2的子元组

#输出
(2, 3)
  1. 列表和元组都可以随意嵌套
# 列表嵌套列表,本质是列表,内部列表和外部列表的元素都可以增删改。也就是二维数组。
l = [[1,2,3],[4,5,6],[7,8,9]]
print('l[0]:',l[0],'\nl[0][0]:',l[0][0])
l[0][0]= 100
print(l)
l[0]= 100
print(l)

l[0]: [1, 2, 3] 
l[0][0]: 1
l: [[100, 2, 3], [4, 5, 6], [7, 8, 9]]
l: [100, [4, 5, 6], [7, 8, 9]]


# 列表嵌套元组,本质是列表,可以对列表中除元组外的其他元素进行增删改。但元组中的内容不可以改变。
l = [(1,2,3),(4,5,6),(7,8,9)]
print('l[0]:',l[0],'\nl[0][0]:',l[0][0])
l[0]= 100
print('l:',l)
l[1][1]= 100
print('l:',l)

l[0]: (1, 2, 3) 
l[0][0]: 1
l: [100, (4, 5, 6), (7, 8, 9)]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-51-29452ab30498> in <module>
      3 l[0]= 100
      4 print('l:',l)
----> 5 l[1][1]= 100
      6 print('l:',l)

TypeError: 'tuple' object does not support item assignment


# 元组嵌套元组,本质元组,元组中的元素还是元组。不能进行任何改变。也就是不可变的二维数组。
l = ((1,2,3),(4,5,6),(7,8,9))
print('l[0]:',l[0],'\nl[0][0]:',l[0][0])
l[0]= 100
print('l:',l)

l[0]: (1, 2, 3) 
l[0][0]: 1
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-53-1a8025802511> in <module>
      1 l = ((1,2,3),(4,5,6),(7,8,9))
      2 print('l[0]:',l[0],'\nl[0][0]:',l[0][0])
----> 3 l[0]= 100
      4 print('l:',l)
      5 l[1][1]= 100

TypeError: 'tuple' object does not support item assignment


# 元组嵌套列表,本质是元组,元组中的任何元素不能进行改变,但是对于元素本身是列表的情况,可以对列表中的值进行增删修。
l = ([1,2,3],[4,5,6],[7,8,9])
print('l[0]:',l[0],'\nl[0][0]:',l[0][0])
l[0][0]= 100
print('l:',l)
l[0]= 100
print('l:',l)

l[0]: [1, 2, 3] 
l[0][0]: 1
l: ([100, 2, 3], [4, 5, 6], [7, 8, 9])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-54-3121a50aeeb4> in <module>
      3 l[0][0]= 100
      4 print('l:',l)
----> 5 l[0]= 100
      6 print('l:',l)

TypeError: 'tuple' object does not support item assignment
  1. 列表和元组常用的内置函数类似
l = [3, 2, 3, 7, 8, 1]
l.count(3) # count(item) 表示统计列表 /元组中 item 出现的次数。
2
l.index(7) # index(item) 表示返回列表 /元组中 item 第一次出现的索引。
3
l.reverse() # list.reverse()表示原地倒转列表。
l
[1, 8, 7, 3, 2, 3]
l.sort() #  list.sort()表示对列表排序
l
[1, 2, 3, 3, 7, 8]
l.sort(reverse=True) # reverse = True表示按降序排列,默认为False(升序)
l
[8, 7, 3, 3, 2, 1]

t = (3, 2, 3, 7, 8, 1)
t.count(3)
2
t.index(7)
3
# 由于元组不可变,故不包含.reverse()和.sort()两个内置函数
# reversed()和 sorted()同样表示对列表 /元组进行倒转和排序,但是会返回一个倒转后或者排好序的新的序列。
list(reversed(t))  # reversed()返回的是reversed对象类型,需要通过list转换才能得到列表
[1, 8, 7, 3, 2, 3]
sorted(t) # 返回的是list数据类型;如果要保留原有的数据类型需要手动转换一下
[1, 2, 3, 3, 7, 8]
sorted(t, reverse=True)
[8, 7, 3, 3, 2, 1]

列表和元组的差异

1.存储方式的差异

l = []
l.__sizeof__()
40
t = ()
t.__sizeof__()
24

同样是创建一个空的数据结构,元组的存储空间,却比列表要少 16 字节。事实上,由于列表是动态的,所以它需要存储指针,来指向对应的元素(int 型指针,占 8 字节),也就是说,元组是直接存储元素,而列表存储的是指向元素的指针。另外,由于列表可变,所以需要存储已经分配的长度大小(这个记录长度的变量占 8 字节),这样才可以实时追踪列表空间的使用情况,当空间不足时,及时分配额外空间。但是对于元组,情况就不同了。元组长度大小固定,元素不可变,所以存储空间固定。

此外,为了减小每次增加 / 删减操作时空间分配的开销,Python 每次给列表分配空间时都会额外多分配一些,这样的机制(称为 over-allocating)保证了其操作的高效性。

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个元素的空间 (104 - 72)/8 = 4

注意,列表的 over-allocate 机制是在你加入了新元素之后,解释器判断得出当前存储空间不够,给你分配额外的空间,因此 l=[ ], l.append(1), l.append(2), l.append(3)实际上在第一次添加元素时就分配了4个元素的空间备用。但是 l=[1, 2, 3] 直接初始化了列表,并没有增加元素的操作,因此只会分配3个元素的空间。

l = [1,2,3]
l.__sizeof__() # 40 + 3*8 = 64
64

l = []
l.__sizeof__() # 空列表的存储空间为40字节,这40字节不是用来存储元素的,本身可变的存储结构都需要一部分内部开销来记录一些特性,就像链表会有头尾指针一样的道理。
40
l.append(1)
l.__sizeof__() 
72 # 加入了元素1之后,列表为其分配了可以存储4个元素的空间 40 + 4*8 = 72
l.append(2) 
l.__sizeof__()
72 # 由于之前分配了空间,所以加入元素2,列表空间不变
l.append(3)
l.__sizeof__() 
72 # 同上

2.性能方面的差异
通过学习列表和元组存储方式的差异,我们可以得出结论:元组要比列表更加轻量级一些,所以总体上来说,元组的性能速度要略优于列表。不过,如果你想要增加、删减或者改变元素,那么列表显然更优。原因就是对于元组,你必须得通过新建一个元组来完成这些改变。

此外,Python 会在后台对静态数据做一些资源缓存(resource caching)。通常来说,因为垃圾回收机制的存在,如果一些变量不被使用了,Python 就会回收它们所占用的内存,返还给操作系统,以便其他变量或其他应用使用。但是对于一些静态变量,比如元组,如果它不被使用并且占用空间不大时,Python 会暂时缓存这部分内存。这样,当下次再创建同样大小的元组时,Python 就可以不用再向操作系统发出请求,去寻找内存,而是可以直接分配之前缓存的内存空间,这样就能大大加快程序的运行速度。

想创建一个空的列表,有两种形式:

# option A
empty_list = list()

# option B
empty_list = []

区别主要在于:list()是一个function call,Python的function call会创建stack,并且进行一系列参数检查的操作,比较耗时,反观 [] 是一个内置的C函数,可以直接被调用,因此效率高。

列表和元组的使用场景

总结一下二者的区别:

  • 列表是动态的,长度可变,可以随意的增加、删减或改变元素。列表的存储空间略大于元组,性能略逊于元组。
  • 元组是静态的,长度大小固定,不可以对元素进行增加、删减或者改变操作。元组相对于列表更加轻量级,性能稍优。

两者可以通过 list() 和 tuple() 函数相互转换。那么列表和元组到底用哪一个呢?具体场景具体分析

  1. 如果存储的数据和数量不变,比如有一个函数,需要返回的是一个地点的经纬度,然后直接传给前端渲染,那么肯定选用元组更合适。也就是说,一般元组用来传参比较多。同时,由于元组是不可变的,元组可以用于做字典的key,这个技巧在数据处理中非常实用。
def get_location():
    ..... 
    return (longitude, latitude)
  1. 如果存储的数据或数量是可变的,比如社交平台上的一个日志功能,是统计一个用户在一周之内看了哪些用户的帖子,那么则用列表更合适。
viewer_owner_id_list = [] # 里面的每个元素记录了这个viewer一周内看过的所有owner的id
records = queryDB(viewer_id) # 索引数据库,拿到某个viewer一周内的日志
for record in records:
    viewer_owner_id_list.append(record.id)

参考

《Python核心技术与实战》

参考源码:
list 和 tuple 的内部实现都是array的形式
列表:https://github.com/python/cpython/blob/master/Objects/listobject.c.
元组: https://github.com/python/cpython/blob/master/Objects/tupleobject.c
头文件
列表:https://github.com/python/cpython/blob/3.7/Include/listobject.h
元组:https://github.com/python/cpython/blob/3.7/Include/tupleobject.h