返回介绍

3.2 列表和元组

发布于 2024-01-25 21:44:08 字数 4873 浏览 0 评论 0 收藏 0

如果列表和元组都使用了相同的数据结构,那么两者之间还有什么区别?主要区别总结如下:

1.列表是动态数组,它们可变且可以重设长度(改变其内部元素的个数)。

2.元组是静态数组,它们不可变,且其内部数据一旦创建便无法改变。

3.元组缓存于Python运行时环境,这意味着我们每次使用元组时无须访问内核去分配内存。

这些区别揭示了两者在设计哲学上的不同:元组用于描述一个不会改变的事物的多个属性,而列表可被用于保存多个互相独立对象的数据集合。比如,保存一个电话号码适合用元组:它们不会改变,如果改变则意味着他们代表了一个新的对象,也就是另一个电话号码。同样,保存一个多项式的系数适合用元组,因为不同的系数代表了不同的多项式。另一方面,保存当前正在阅读本书的人的名字更适合用列表:虽然数据的内容和大小时刻在发生变化,但始终表示同一个概念。

值得提醒的是,列表和元组都可以接受混合类型。我们会看到,这会带来一些额外的开销并减少一些可能的优化。如果我们强制要求所有的数据都是同一个类型,那么就可以避免这些开销。我们将在第6章讨论如何通过使用numpy降低内存和计算的开销。另外,对于非数字的数据,还有一些其他模块,如blist和array也能够减少这些开销。这暗示了我们将在后续章节介绍的高性能编程的一个主要要点:通用代码会比为某个特定问题设计的代码慢很多。

另外,跟列表可以改变大小及内容不同,元组的不可改变性使其成为了一个非常轻量级的数据结构。这意味着存储它们不需要很多的内存开销,而且对它的操作也非常的直观。我们将会看到列表的可变性的代价在于存储它们需要额外的内存以及使用它们需要额外的计算。

 问题 

对于下面的示例数据集,你会选择元组还是列表?为什么?

1.前20个质数。

2.编程语言的名字。

3.一个人的年龄、体重、身高。

4.一个人的生日和出生地。

5.某次台球游戏的结果。

6.一系列台球游戏的结果。

答案:

1.元组,因为数据是静态的且不会改变。

2.列表,因为数据集会不停增长。

3.列表,因为这些值会被更新。

4.元组,因为这些信息是静态的且不会改变。

5.元组,因为数据是静态的。

6.列表,因为会有更多游戏进行(事实上,我们可以使用一个元组的列表。因为单个游戏的数据不会改变,但是随着游戏次数的上升,我们会需要增加列表的长度)。

3.2.1 动态数组:列表

一旦我们创建了列表,我们就可以根据需要随意改变其内容:

>>> numbers = [5, 8, 1, 3, 2, 6]
>>> numbers[2] = 2*numbers[0] <em># </em>❶
>>> numbers
[5, 8, 10, 3, 2, 6]

❶ 如前所述,这个操作是O(1),因为我们可以立即找到第0个和第2个数据保存的位置。

另外,我们可以给列表添加新的数据来增加其大小:

>>> len(numbers)
6
>>> numbers.append(42)
>>> numbers
[5, 8, 10, 3, 2, 6, 42]
>>> len(numbers)
7

这是因为动态数组支持resize操作,可以增加数组的容量。当一个大小为N的列表第一次需要添加数据时,Python会创建一个新的列表,足够存放原来的N个元素以及额外需要添加的元素。不过,实际分配的并不是N+1个元素,而是M个,M > N,这是为了给未来的添加预留空间。然后旧列表的数据被复制到新列表中,旧列表则被销毁。从设计理念上来说,第一次的添加可能会是后续多次添加的开始,通过预留空间的做法,我们就可以减少这一分配空间的操作的次数以及内存复制的次数。这一点非常重要,因为内存复制可能非常昂贵,特别是当列表大小开始增长以后。图3-2显示了在Python 2.7中这一超额分配的做法。分配空间的公式见例3-5。

图3-2 图中显示了对于一个特定大小的列表会分配多少个额外的元素

例3-5 Python 2.7的列表空间分配公式

M = (N >> 3) + (N < 9 ? 3 : 6)
 N  0  1-4 5-8 9-16 17-25 26-35  36-46 … 991-1120
                        
 M  0  4   8  16  25  35   46  … 1120

当我们需要添加数据时,我们可以直接利用额外的空间并增加列表的有效容量,N。我们继续添加数据,N会继续增长直到N == M。此时,没有额外的空间给我们插入,我们必须创建一个拥有更多额外空间的新列表。这个新列表的额外空间大小如例3-5的公式所示,然后我们将旧数据复制进新的空间。

这一系列的事件见图3-3。该图显示了例3-6中列表l的各种操作。

例3-6 列表大小的改变

l = [1, 2]
for i in range(3, 7):
  l.append(i)

 备忘 

这一超额分配发生在第一次往列表里添加元素时。在一个列表被直接创建时,如前例,分配的元素数量是完全按需的。

额外分配的空间一般来说非常小,但累加起来就不可忽视。当你在维护很多小列表或一个非常大的列表时,这一效果会变得十分显著。如果我们维护1 000 000个列表,每个列表都包含10个元素,那么我们可能会假设占用了10 000 000个元素的内存。但是如果在构建列表时用了append操作,实际占用的内存可能是16 000 000个元素。同样,对于一个拥有100 000 000个元素的大列表,实际分配的可能是112 500 007个元素!

图3-3 一个列表在多次添加时的变化示例

3.2.2 静态数组:元组

元组固定且不可变。这意味着一旦元组被创建,和列表不同,它的内容无法被修改或它的大小也无法被改变:

>>> t = (1,2,3,4)
>>> t[0] = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

虽然它们不支持改变大小,但是我们可以将两个元组合并成一个新元组。这一操作类似列表的resize操作,但我们不需要为新生成的元组分配任何额外的空间:

>>> t1 = (1,2,3,4)
>>> t2 = (5,6,7,8)
>>> t1 + t2
(1, 2, 3, 4, 5, 6, 7, 8)

如果我们将其跟列表的append操作比较,我们会看到它的复杂度是O(n)而不是列表的O(1)。这是因为对元组每添加一个新元素都会有分配和复制操作,而不是像列表那样仅在额外的空间耗尽时发生。所以,元组并没有提供一个类似append的自增操作,任意两个元组相加始终返回一个新分配的元组。

不为改变大小保存额外空间带来的好处是使用了更少的资源。一个使用过append操作的大小为100000000的列表实际上占用了112500007的元素的内存,而保存同样数据的元组始终占用100000000个元素的内存。这使得元组对于静态数据是一个轻量级且更好的选择。

另外,即使我们创建的列表并没有使用append(也就是并没有append操作导致的额外空间),它占用的内存依然大于保存同样数据的元组。这是因为列表需要记住更多关于它们自身状态的信息来进行高效的resize。虽然这一额外的信息很少(仅一个额外元素),如果我们有几百万个列表,累加起来也不可忽视。

元组的静态特性的另一个好处体现在一些会在Python后台发生的事:资源缓存。Python是一门垃圾收集语言,这意味着当一个变量不再被使用时,Python会将该变量使用的内存释放回操作系统,以供其他程序(或变量)使用。然而,对于长度为1~20的元组,即使它们不再被使用,它们的空间也不会立刻被还给系统,而是留待未来使用。这意味着当未来需要一个同样大小的新元组时,我们不再需要向操作系统申请一块内存来存放数据,因为我们已经有了预留的内存。

这看上去可能只是一个细微的好处,但实际上是元组一个很神奇的地方:它们可以被轻松迅速地创建,因为它们可以避免跟操作系统打交道,而后者很花时间。例3-7显示了初始化一个列表比初始化一个元组慢5.1倍——如果是在一个循环内部,这点差别会很快累加起来!

例3-7 初始化列表和元组的时间对比

>>> %timeit l = [0,1,2,3,4,5,6,7,8,9]
1000000 loops, best of 3: 285 ns per loop
>>> %timeit t = (0,1,2,3,4,5,6,7,8,9)
10000000 loops, best of 3: 55.7 ns per loop

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文