在本博客中,咱们将学习探讨Python的各类“序列”类,内置的三大经常使用数据结构——列表类(list)、元组类(tuple)和字符串类(str)的本质。html
不知道你发现没有,这些类都有一个很明显的共性,均可以用来保存多个数据元素,最主要的功能是:每一个类都支持下标(索引)访问该序列的元素,好比使用语法 Seq[i]
。其实上面每一个类都是使用 数组
这种简单的数据结构表示。python
可是熟悉Python的读者可能知道这3种数据结构又有一些不一样:好比元组和字符串是不能修改的,列表能够修改。shell
计算机体系结构中,咱们知道计算机主存由位信息组成,这些位一般被归类成更大的单元,这些单元则取决于精准的系统架构。一个典型的单元就是一个字节,至关于8位。编程
计算机系统拥有庞大数量的存储字节,那么如何才能找到咱们的信息存在哪一个字节呢?答案就是你们平时熟知的 存储地址
。基于存储地址,主存中的任何字节都能被有效的访问。实际上,每一个存储字节都和一个做为其地址的惟一二进制数字相关联。以下图中,每一个字节均被指定了存储地址:数组
通常来讲,编程语言记录标识符和其关联值所存储的地址之间的关系。好比,当咱们声明标识符 \(x\) 就有可能和存储器中的某一值相关联,而标识符 \(y\)就可能和其余的值相关联。一组相关的变量可以一个接一个地存储在计算机存储器的一块连续区域内。咱们将这种方式称为 数组。数据结构
咱们来看Python中的例子,一个文本字符串 HELLO
是以一列有序字符的形式存储的,假定该字符串的每一个Unicode字符须要两个字节的存储空间。最下面的数字就是该字符串的索引值。架构
咱们能够看到,数组能够存储多个值而无需构造具备特定索引的多个变量来指定其中的每一个项目,而且几乎在全部编程语言(例如C、Java、C#、C++)中使用,可是Python更具备优点。Python在构建列表时,熟悉的读者可能知道,不须要预先定义数组或列表的大小,相反,在Python中,列表具备动态性质,咱们能够不断的往列表中添加咱们想要的数据元素。接下来,让咱们看看Python列表的知识(已经熟悉的读者能够快速浏览或者跳过)。app
[ele1, ele2, ele3, ele4, ...]
编程语言
(ele1, ele2, ele3, ele4, ...)
编辑器
元组比列表的内存空间利用率更高,由于元组是固定不变的,因此没有必要建立拥有剩余空间的动态数组。
咱们先在Python的IDE中建立一个列表,而后大体了解一下列表部份内置操做,咱们先建立了一个名为test_list的列表,而后修改(插入或删除)元素,反转或清空列表,具体以下:
>>> test_list = [] # 建立名为test_list的空列表 >>> test_list.append("Hello") >>> test_list.append("World") >>> test_list ['Hello', 'World'] >>> test_list = ["Hello", "Array", 2019, "easy learning", "DataStructure"] # 从新给test_list赋值 >>> len(test_list) # 求列表的长度 5 >>> test_list[2] = 1024 # 修改列表元素 >>> test_list ['Hello', 'Array', 1024, 'easy learning', 'DataStructure'] >>> >>> test_list.insert(1, "I love") # 向列表中指定位置中插入一个元素 >>> test_list ['Hello', 'I love', 'Array', 1024, 'easy learning', 'DataStructure'] >>> test_list.append(2020) # 向列表末尾增长一个元素 >>> test_list ['Hello', 'I love', 'Array', 1024, 'easy learning', 'DataStructure', 2020] >>> >>> test_list.pop(1) # 删除指定位置的元素 'I love' >>> test_list.remove(2020) # 删除指定元素 >>> >>> test_list.index('Hello') # 查找某个元素的索引值 0 >>> test_list.index('hello') # 若是查找某个元素不在列表中,返回ValueError错误 Traceback (most recent call last): File "<pyshell#11>", line 1, in <module> test_list.index('hello') ValueError: 'hello' is not in list >>> >>> test_list.reverse() # 反转整个列表 >>> test_list ['DataStructure', 'easy learning', 2019, 'Array', 'Hello'] >>> test_list.clear() # 清空列表 >>> test_list []
咱们看上面的代码,能够看到list的相关操做——增删改查,已经很强大了,还有一些内置方法这里并无作展现,留给读者本身去发现并体验。
所以,让咱们经过编码实践以及内存中保存的数组的实际大小与给定大小之间的关系来查看这种额外的空间演示。
前往Jupyter notebook进行练习。或者使用本身选择的任何编辑器或开发环境。复制下面编写的代码。
# 导入sys模块能方便咱们使用gestsizeof函数 import sys # set n n = 20 # set empty list list = [] for i in range(n): a = len(list) # 调用getsizeof函数用于给出Python中存储对象的真实字节数 b = sys.getsizeof(list) print('Length:{0:3d}; Size of bytes:{1:4d}'.format(a, b)) # Increase length by one list.append(n)
运行代码,能够看到以下输出:
如今,随着咱们增长列表的长度,字节也增长了。咱们分析一下,Length:1
位置的元素填入列表时,字节数从64跳到96,增长了32个字节。由于本实验是在64位机器上运行的,这代表每一个内存地址是64位(即8个字节)。增长的32个字节即为分配的用于存储4个对象引用的数组大小。当增长第2个、第3个或者第4个元素时,内存占用没有任何改变。字节数96可以提供4个对象的引用。
\[ 96\ =\ 64\ +\ 8\ \times \ 4 \]
当Length:10
时,字节数从一开始的64跳到192,能存下16个对象的引用,
\[ 192\ =\ 64\ +\ 8\ \times \ 16 \]
一直到Length: 17
后又开始跳转,因此理论上264个字节数应该能够存下25个对象
\[ 264\ =\ 64\ +\ 8\ \times \ 25 \]
但由于咱们在代码中设置n=20
,而后程序就终止了。
咱们能够看到Python内置的list类足够智能,知道当须要额外的空间来分配数据时,它会为它们提供额外的大小,那么这到底是如何被实现的呢?
好吧,答案是动态数组。说到这里,不知道你们学Python列表的时候是否是这样想的——列表很简单嘛,就是list()类、用中括号[]括起来,而后指导书籍或文档上的各种方法append、insert、pop...在各类IDE一顿操做事后,是的我以为我学会了。
但其实背后的原理真的很不简单,好比我举个例子:A[-1]
这个操做怎么实现?列表切片功能怎么实现?如何本身写pop()默认删除列表最右边的元素(popleft删除最左边简单)?...这些功能用起来爽,但真的本身实现太难了(我也还在学习中,大佬们请轻喷!)若是咱们能学习并理解,确定能够增强咱们对数组这一结构的理解。
动态数组是内存的连续区域,其大小随着插入新数据而动态增加。在静态数组中,咱们须要在分配时指定大小。在定义数组的时候,其实计算机已经帮咱们分配好了内存来存储,实际上咱们不能扩展数组,由于它的大小是固定的。好比:咱们分配一个大小为10的数组,则不能插入超过10个项目。
可是动态数组会在须要的时候自动调整其大小。这一点有点像咱们使用的Python列表,能够存储任意数量的项目,而无需在分配时指定大小。
因此实现一个动态数组的实现的关键是——如何扩展数组?当列表list1的大小已满时,而此时有新的元素要添加进列表,咱们会执行一下步骤来克服其大小限制的缺点:
接下来要思考的问题是,新数组应该多大?一般咱们得作法是:新数组的大小是已满的旧数组的2倍。咱们将在Python中编程实现动态数组的概念,并建立一个简单的代码,不少功能不及Python强大。
在Python中,咱们利用ctypes的内置库来建立本身的动态数组类,由于ctypes模块提供对原始数组的支持,为了更快的对数组进行学习,因此对ctypes的知识能够查看官方文档进行学习。关于Python的公有方法与私有方法,咱们在方法名称前使用双下划线**__**使其保持隐藏状态,代码以下:
# -*- coding: utf-8 -*- # @Time : 2019-11-01 17:10 # @Author : yuzhou_1su # @ContactMe : https://blog.csdn.net/yuzhou_1shu # @File : DynamicArray.py # @Software : PyCharm import ctypes class DynamicArray: """A dynamic array class akin to a simplified Python list.""" def __init__(self): """Create an empty array.""" self.n = 0 # count actual elements self.capacity = 1 # default array capacity self.A = self._make_array(self.capacity) # low-level array def is_empty(self): """ Return True if array is empty""" return self.n == 0 def __len__(self): """Return numbers of elements stored in the array.""" return self.n def __getitem__(self, i): """Return element at index i.""" if not 0 <= i < self.n: # Check it i index is in bounds of array raise ValueError('invalid index') return self.A[i] def append(self, obj): """Add object to end of the array.""" if self.n == self.capacity: # Double capacity if not enough room self._resize(2 * self.capacity) self.A[self.n] = obj # Set self.n index to obj self.n += 1 def _resize(self, c): """Resize internal array to capacity c.""" B = self._make_array(c) # New bigger array for k in range(self.n): # Reference all existing values B[k] = self.A[k] self.A = B # Call A the new bigger array self.capacity = c # Reset the capacity @staticmethod def _make_array(c): """Return new array with capacity c.""" return (c * ctypes.py_object)() def insert(self, k, value): """Insert value at position k.""" if self.n == self.capacity: self._resize(2 * self.capacity) for j in range(self.n, k, -1): self.A[j] = self.A[j-1] self.A[k] = value self.n += 1 def pop(self, index=0): """Remove item at index (default first).""" if index >= self.n or index < 0: raise ValueError('invalid index') for i in range(index, self.n-1): self.A[i] = self.A[i+1] self.A[self.n - 1] = None self.n -= 1 def remove(self, value): """Remove the first occurrence of a value in the array.""" for k in range(self.n): if self.A[k] == value: for j in range(k, self.n - 1): self.A[j] = self.A[j+1] self.A[self.n - 1] = None self.n -= 1 return raise ValueError('value not found') def _print(self): """Print the array.""" for i in range(self.n): print(self.A[i], end=' ') print()
上面咱们已经实现了一个动态数组的类,相信都很激动,接下来让咱们来测试一下,看能不能成功呢?在同一个文件下,写的测试代码以下:
def main(): # Instantiate mylist = DynamicArray() # Append new element mylist.append(10) mylist.append(9) mylist.append(8) # Insert new element in given position mylist.insert(1, 1024) mylist.insert(2, 2019) # Check length print('The array length is: ', mylist.__len__()) # Print the array print('Print the array:') mylist._print() # Index print('The element at index 1 is :', mylist[1]) # Remove element print('Remove 2019 in array:') mylist.remove(2019) mylist._print() # Pop element in given position print('Pop pos 2 in array:') # mylist.pop() mylist.pop(2) mylist._print() if __name__ == '__main__': main()
激动人心的时刻揭晓,测试结果以下。请结合测试代码和数组的结构进行理解,若是由疏漏,欢迎你们指出。
The array length is: 5 Print the array: 10 1024 2019 9 8 The element at index 1 is : 1024 Remove 2019 in array: 10 1024 9 8 Pop pos 2 in array: 10 1024 8
经过以上的介绍,咱们知道了数组存在静态和动态类型。对应到Python——列表就是动态数组,而元组和字符串就是静态数组。 而在本博客中,咱们着重介绍了什么是动态数组,并经过Python代码进行实现。但愿你能今后以复杂的方式学会数组。 总结发言,看似简单的操做,背后实现原理可能很复杂。