Python进阶之列表与元组精析


Python有五大数据结构: 列表,元组,字典,集合以及字符串。了解掌握这些数据结构对于学好Python至关重要。

1.数组数据结构

大多数的编程语言都会有数组这种数据结构,并且它们的数组会要求数组元素类型一致。但是Python并没有这样的要求,实际上,Python中的列表和元组就是一个可以放置任意数据类型的有序数组。

例如下面这段代码,就分别定义了含有不同类型元素的列表和元组:

#定义一个同时含有int和str类型的列表
l = [1,2,3,"hello"]
#定义一个同时含有int和str类型的元组
t = (1,2,3,"world")

2.列表和元组基础

列表和元组的基本操作可以参见下面这篇文章:
https://blog.csdn.net/PecoHe/article/details/89923961

由上面的文章我们可以知道:

  • 列表就是一种可变的动态数组,它以动态数组实现的,这说明它的长度大小不固定,可以随意地增加、删减或者改变元素,也可以分配或者释放内存来自动调整存储空间。
  • 元组是一种不可变容器,它是静态的,这意味着它长度大小固定,无法增加删减或者改变,并且元组中所有的元素需要在元组创建的时候定义。如果想对已有的元组做任何改变,只能重新开辟一块内存,创建新的元组。

列表和元组可以通过list()和tuple()函数相互转换,例如下面的代码:

#定义一个同时含有int和str类型的列表
l = [1,2,3,"hello"]
tuple(l)
#定义一个同时含有int和str类型的元组
t = (1,2,3,"world")
list(t)

3.列表和元组存储方式深究

我们知道列表和元组最重要的区别就是列表是动态的,其中的元素可变,而元组是静态的,元组中的元素不可变。这样的差异就会影响这两种数据结构的存储方式

首先来观察一下两者占内存空间大小的情况:

#test list
l1=[]
print("The size of empty list is {}.".format(l1.__sizeof__()))
l = [1,2,3]
print("The size of 3 elements list is {}.".format(l.__sizeof__()))

#test tuple
t1=()
print("The size of empty tuple is {}.".format(t1.__sizeof__()))
t = (1,2,3)
print("The size of 3 elements tuple is {}.".format(t.__sizeof__()))

输出结果如下:

The size of empty list is 40.
The size of 3 elements list is 64.

The size of empty tuple is 24.
The size of 3 elements tuple is 48.

由上面的结果不难发现,含有三个元素,零个元素的列表占用空间都比含有对应个数元素的元组多用了16个字节。在理解为什么会这样之前,先来看下面这段代码和它的输出:

a=1
print(a.__sizeof__())
test_list = (1,1,1)
print(test_list.__sizeof__())

输出:

28
48

从这个结果中可以看出,一个int型的1就占了28个字节,为什么列表里存放了3个int才占48个字节呢?带着这样的问题,我们来观察一下这两个数据结构的实现代码:

列表:https://github.com/python/cpython/blob/master/Include/listobject.h

typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     * list.sort() temporarily sets allocated to -1 to detect mutations.
     *
     * Items must normally not be NULL, except during construction when
     * the list is not yet visible outside the function that builds it.
     */
    Py_ssize_t allocated;
} PyListObject;

通过上面PyListObject的源代码可以看到,list本质上是一个超额分配的数组(类似于C++ STL中的vector内存分配方式,即申请1个空间的时候会实际分配4倍的空间,这样在后续没有超过该空间的插入操作时可以不用重新分配,从而提升性能)。

其中,ob_item是一个占8个字节的二重指针,其本质代表着一个指针数组的头地址。我们知道在C语言中定义一个 int a[10],那么a本质就是指向这个数组头地址一个指针,可以通过a[i]来访问位置i处的元素,ob_item就是这个样子,ob_item[i]就存放这列表第i项元素的地址。

allocated是一个整型数,它代表着这个列表当前已经分配的空间大小,即这个数规定了以ob_item为数组头地址的指针数组的大小。需要注意的allocated 与列表实际使用空间大小的区别,列表实际空间大小,是指len(list)返回的结果,即上述代码注释中的ob_size,这个值实际规定了通过ob_item[i]来访问数组内元素的实际i的范围,表示这个列表总共存储了多少个元素。实际情况下,为了优化存储结构,避免每次增加元素都要重新分配内存,列表预分配的空间allocated往往会大于ob_size(详见正文中的例子)。所以,它们的关系为:allocated >= len(list) = ob_size。

经过上面的分析我们可以得出,list存放的并非实际的整数,而是每个元素的地址。

这里还有一个细节,就是对于列表来说,初始的大小是跟定义方式有关。
如果一开始定义一个空的列表,那么列表初始大小为40,后续通过append函数往里面添加值时,列表会首先申请4个元素的空间,然后直到4个用完了才会申请更多额外的空间,具体申请的大小不固定,下面是一段测试申请空间大小的代码:

test_list=[]
test_list.__sizeof__()
for i in range (100):
    pre = (test_list.__sizeof__()-40)/8
    test_list.append(1)   
    add = (test_list.__sizeof__()-40)/8-pre
    if add>0:
        print("now add {}.".format(add))

输出结果:

40
now add 4.0.
now add 4.0.
now add 8.0.
now add 9.0.
now add 10.0.
now add 11.0.
now add 12.0.
now add 14.0.
now add 16.0.
now add 18.0.

而如果一开始就直接给元素赋值,那么列表的初始大小就是
元 素 的 个 数 ∗ 8 + 40 元素的个数 * 8+40 8+40
后面同样会按照上述的方式来申请空间。


下面再来看看元组的源代码。

元组:https://github.com/python/cpython/blob/master/Include/cpython/tupleobject.h

typedef struct {
    PyObject_VAR_HEAD
    /* ob_item contains space for 'ob_size' elements.
       Items must normally not be NULL, except during construction when
       the tuple is not yet visible outside the function that builds it. */
    PyObject *ob_item[1];
} PyTupleObject;

从上面的代码中可以看到,元组里也有一个数组,但是它的大小固定为1。

事实上,由于列表是动态的,所以它需要存储指针,来指向对应的元素(上述例子中,对于int型,8字
节)。另外,由于列表可变,所以需要额外存储已经分配的长度大小(8字节),这样才可以实时追踪列表空间的使用情况,当空间不足时,及时分配额外空间。上面的例子,大概描述了列表空间分配的过程。我们可以看到,为了减小每次增加/删减操作时空间分配的开销,Python每次分配空间时都会额外多分配一些,这样的机制保证了其操作的高效性:增加/删除的时间复杂度均为O(1)。

但是对于元组,情况就不同了。元组长度大小固定,元素不可变,所以存储空间固定。
看了前面的分析,你也许会觉得,这样的差异可以忽略不计。但是想象一下,如果列表和元组存储元素的个数是一亿,十亿甚至更大数量级时,你还能忽略这样的差异吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值