摘要
首先说明,以下几类读者请自行对号入座:
- 刚接触编程并对底层原理知之甚少,但又想了解一下数据结构的读者,强烈建议阅读此篇;
- 计算机专业在读,对编程有一定了解,但对数据结构不够熟悉的读者,强烈建议阅读此篇;
- 传统运维想要快速上手自动化,没有精力对底层原理做深入了解的,请跳过此番外篇系列;
- 计算机专业出身并对数据结构有系统了解的读者,请跳过此系列番外篇。
我们在【自动化运维新手村】初见Python 一文中涉及到了Python中的列表和字典,我们提到列表是有序的,而字典不是有序的,大家可能对这个概念会有疑问?
列表有序性并不是指存储的元素是按顺序排列的,而是指元素存储的顺序会保持不变,任何时候遍历都会是创建时的顺序;但字典却不是,在Python3.6之前,字典在每次遍历的时候顺序都可能是不同的(仅限于3.6版本之前,之后的版本字典就变为有序字典,所以再看到有的文章盲目说Python是无序的,那要么是这个文章太老了,要不就....)
今天我们就带着大家从计算机的底层结构来深入理解一下Python中的列表和字典。
数组(Array)
**Python中的列表对应的数据结构就是数组,**数组是一种十分常见的数据结构,属于线性表的一种。在计算机中的存储的样子大概是如下:
数组可以实现的操作有下面几种:
- 存储
- 按下标索引
- 插入和删除
- 修改
- 遍历
那我们就根据数组的存储原理来依次讲解:
1. 存储
任何一种数据结构在计算机中存储的时候都需要考虑所占用空间大小的问题,那么数组究竟占用多少空间呢?
在数据结构中,数组中只能存储相同类型的元素,所以整个数组的空间大小其实取决于数组中元素的类型和元素的数量,不同数据类型占用的空间如下图:
所以,假设我们定义了一个10个长度的int类型的数组变量 int_array,那么它在内存中占用的空间大小为40个字节,并且是连续的40个字节。
2. 索引
数组一个很大的好处就是可以按下标进行索引,假设我们有一个数组int_array = [1, 2, 3, 4, 5, 6],那么我们可以直接根据下标取到对应的元素,比如int_array[3],就是取下标为3的元素。
划重点:数组下标从0开始
有个有意思的梗:想要看一个人是不是程序员,只要让他数十个数就可以,如果他从0开始数,那就铁定是程序员了。
很多人可能也会有疑问,为什么下标会从0开始,其实这也和数据的底层存储有关。
我们这里定义一个具有10个int元素的数组变量int_array,当我们定义一个变量时,这个变量在内存中就有了一个地址,我们假设它为base_address,那么int_array所占的内存空间就是从base_address到base_address+10*4,10为元素数量,4为元素类型大小。
由此可知,当我们要找int_array的第一个元素时,我们需要知道他的内存地址,他的内存地址的计算方法就是
int_array[i]_address = base_address + i * sizeOf(int)
所以数组中我们常说的下标其实也叫做偏移量,想要按下标寻址某个元素,就是知道它的偏移量,那么理所当然数组中第一个元素肯定偏移量是0。
3. 插入删除
数组的插入和删除又分为在末尾进行插入和删除,以及在中间进行插入和删除
3.1 数组尾部插入和删除
我们根据上面的介绍已经知道了数组在内存中是一块连续的空间,所以当在数组的尾部进行插入和删除时,不需要任何额外的操作,时间复杂度为O(1)。
3.2 数组中间插入和删除
当我们在数组的中间进行插入时,我们需要将插入位置之后的元素都向后挪动一位,然后将要插入的元素放在指定下标处;
而在数组中间进行删除时,为了保证数组内存空间的连续性,就需要将该下标位置之后的元素依次向前挪动一位;
当在数组的0位置处进行插入和删除时,性能耗费最大,时间复杂度为O(n)
4. 修改
数组的修改的话其实就是先根据下标索引到该元素所在的内存地址,然后再对其进行修改,我们这里不做赘述。
5. 遍历
由于数组内存空间的连续性,我们创建数组时元素的排列,无论何时遍历都可以保证是相同的,所以我们称数组具有有序性。
列表(List)
由于Python中的列表对应的数据结构就是数组,所以我们搞清楚了数组的原理,对于列表的理解就会容易很多。
Python的列表具有和数组相同的操作方法,即遍历,索引,插入,删除,但也有不同之处
1. 元素
我们默认数据结构中的数组所存储的元素都是相同类型的,事实上很多语言,例如C,Golang,Java等都要求在定义数组时声明其类型,比如Golang的数组创建的语法就为var intArray = []int{},但Python做为弱类型语言,并不要求类型定义,所以我们可以定义一个数组,存储任意类型的元素类型,如下:
>>> array = [1]
>>> array.append("2")
# array 为 [1, "2"]
>>> array.append([3, 4])
# array 为 [1, "2", [3, 4]]
所以Python中的列表虽然仍保留了数组的空间连续性,但它所占用的空间大小,却是和数组中存储的元素类型密切相关。
2. 长度
我们默认数据结构中的数组在定义时需要初始化其长度,也就是其存储的元素数量,这样方便程序为其分配固定大小的内存空间,如C语言中数组的定义为int a[10]={0};,含义为初始化一个包含10个int类型元素的数组,每个元素默认赋值为0。
但Python在定义数组时并不要求定义其长度,语法为array = [],我们定义了一个数组变量为array,后续可以向其任意添加元素。
// List列表的具体结构如下所示
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;
Python中的List本质上是一个长度可变的连续数组。其中存在一个指针列表ob_item,里边的每一个指针都指向列表中的元素,而 allocated 则用于存储该列表目前已被分配的空间大小。
需要注意的是,allocated 和列表的实际空间大小不同,列表实际空间大小,指的是 len(list) 返回的结果,也就是上边代码中注释中的 ob_size,表示该列表总共存储了多少个元素。而在实际情况中,为了优化存储结构,避免每次增加元素都要重新分配内存,列表预分配的空间 allocated 往往会大于 ob_size。
因此 allocated 和 ob_size 的关系是:allocated >= len(list) = ob_size >= 0。
如果当前列表分配的空间已满(即 allocated == len(list)),则会向系统请求更大的内存空间,并把原来的元素全部拷贝过去。
这篇文章我们着重理解了数组的底层原理以及Python中的List,这对于后续我们的编程会起到很大的帮助,如果把用Python编程是在计算机上蒙了一块黑布的话,那么对于计算机底层原理的学习,就会让我们一点点揭开这块黑布,最终直到我们写下每一行代码都能够清晰的了解到其真正的底层运行逻辑。
欢迎大家添加我的个人公众号【Python玩转自动化运维】加入读者交流群,获取更多干货内容