Python序列类型的本质

在本博客中,我们将学习探讨Python的各种“序列”类,内置的三大常用数据结构——列表类(list)、元组类(tuple)和字符串类(str)的本质。

不知道你发现没有,这些类都有一个很明显的共性,都可以用来保存多个数据元素,最主要的功能是:每个类都支持下标(索引)访问该序列的元素,比如使用语法 Seq[i]。其实上面每个类都是使用 数组 这种简单的数据结构表示。

但是熟悉Python的读者可能知道这3种数据结构又有一些不同:比如元组和字符串是不能修改的,列表可以修改。

计算机内存中的数组结构

计算机体系结构中,我们知道计算机主存由位信息组成,这些位通常被归类成更大的单元,这些单元则取决于精准的系统架构。一个典型的单元就是一个字节,相当于8位。

计算机系统拥有庞大数量的存储字节,那么如何才能找到我们的信息存在哪个字节呢?答案就是大家平时熟知的 存储地址 。基于存储地址,主存中的任何字节都能被有效的访问。实际上,每个存储字节都和一个作为其地址的唯一二进制数字相关联。如下图中,每个字节均被指定了存储地址:

一般来说,编程语言记录标识符和其关联值所存储的地址之间的关系。比如,当我们声明标识符 \(x\) 就有可能和存储器中的某一值相关联,而标识符 \(y\)就可能和其他的值相关联。一组相关的变量能够一个接一个地存储在计算机存储器的一块连续区域内。我们将这种方式称为 数组

我们来看Python中的例子,一个文本字符串 HELLO 是以一列有序字符的形式存储的,假定该字符串的每个Unicode字符需要两个字节的存储空间。最下面的数字就是该字符串的索引值。

我们可以看到,数组可以存储多个值而无需构造具有特定索引的多个变量来指定其中的每个项目,并且几乎在所有编程语言(例如C、Java、C#、C++)中使用,但是Python更具有优势。Python在构建列表时,熟悉的读者可能知道,不需要预先定义数组或列表的大小,相反,在Python中,列表具有动态性质,我们可以不断的往列表中添加我们想要的数据元素。接下来,让我们看看Python列表的知识(已经熟悉的读者可以快速浏览或者跳过)。

Python列表

Python列表的操作

  • 创建列表的语法格式:

[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的相关操作——增删改查,已经很强大了,还有一些内置方法这里并没有做展示,留给读者自己去发现并体验。

Python列表的内存分配背后的基础知识

因此,让我们通过编码实践以及内存中保存的数组的实际大小与给定大小之间的关系来查看这种额外的空间演示。

前往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的大小已满时,而此时有新的元素要添加进列表,我们会执行一下步骤来克服其大小限制的缺点:

  1. 分配具有更大容量的新数组 list2
  2. 设置 list2[i] = list1[i] (i=0,1,2,...,n-1),其中n是该项目的当前编号
  3. 设置list1 = list2,也就是说,list2正在作为新的数组来引用我们的新列表。
  4. 然后,只要将新的元素插入(添加)到我们的列表list1即可。

接下来要思考的问题是,新数组应该多大?通常我们得做法是:新数组的大小是已满的旧数组的2倍。我们将在Python中编程实现动态数组的概念,并创建一个简单的代码,很多功能不及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()

测试动态数组Python代码

上面我们已经实现了一个动态数组的类,相信都很激动,接下来让我们来测试一下,看能不能成功呢?在同一个文件下,写的测试代码如下:

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代码进行实现。希望你能从此以复杂的方式学会数组。

总结发言,看似简单的操作,背后实现原理可能很复杂。

用最复杂的方式学会数组(Python实现动态数组)的更多相关文章

  1. go 动态数组 二维动态数组

    go使用动态数组还有点麻烦,比python麻烦一点,需要先定义. 动态数组申明 var dynaArr []string 动态数组添加成员 dynaArr = append(dynaArr, &quo ...

  2. DelphiXe 中静态数组TByteArray和动态数组TBytes /array of byte 的区别

    在应用中发现静态数组和动态数组是有区别的: procedure TForm1.Button1Click(Sender: TObject);var  RsltStream: TMemoryStream; ...

  3. python数据结构之动态数组

    数组列表:动态数组(Array List) 简介: 最基础简单的数据结构.最大的优点就是支持随机访问(O(1)),但是增加和删除操作效率就低一些(平均时间复杂度O(n)) 动态数组也称数组列表,在py ...

  4. 常用数据结构-线性表及Java 动态数组 深究

    [Java心得总结六]Java容器中——Collection在前面自己总结的一篇博文中对Collection的框架结构做了整理,这里深究一下Java中list的实现方式 1.动态数组 In compu ...

  5. C++之动态数组

    C99支持一种名为变长数组的结构来方便程序员.C++也提供了一种长度可在程序运行时确定的数组类型:动态数组.声明格式为:(声明 int 类型的数组) ; //此处可修改 ArraySize 的值 in ...

  6. VB默认属性、动态数组、Range对象的默认属性的一点不成熟的想法

    1.默认属性 VB6.0有默认属性的特性.当没有给对象指定具体的属性时,"默认属性"是VB6.0将使用的属性.在某些情形下,省略常用属性名,使代码更为精简. 因为CommandBu ...

  7. VB.NET 数组的定义 动态使用 多维数组

    我们都知道在全部程序设计语言中数组都是一个非常重要的概念,数组的作用是同意程序猿用同一个名称来引用多个变量,因此採用数组索引来区分这些变量.非常多情况下利用数组索引来设置一个循环,这样就能够高效地处理 ...

  8. ALLOCATE语句分配FORTRAN动态数组方法(转自http://blog.csdn.net/zhuxianjianqi/article/details/8067174)

    数组的动态分配 a)    可分配数组 数组可以是静态的也可以是动态的.如果数组是静态的,则在编译时就被分配了固定的储存空间,并且直到程序退出时才被释放.程序运行时静态数组的大小不能改变.静态数组的缺 ...

  9. 线性表之顺序存储结构(C语言动态数组实现)

    线性表的定义:N个数据元素的有限序列 线性表从存储结构上分为:顺序存储结构(数组)和 链式存储结构(链表) 顺序存储结构:是用一段连续的内存空间存储表中的数据 L=(a1,a2,a3....an) 链 ...

随机推荐

  1. 验证fstab文件修改是否正确

    /etc/fstab文件是系统启动时要挂载点文件系统. 如果fstab文件配置错误,会导致系统无法正常启动. 最近我就是因为大意,在配置fstab重启服务器后,出现系统无法识别挂载点/opt/back ...

  2. win10下安装FFmpeg步骤

    1.官方下载地址:https://ffmpeg.zeranoe.com/builds/ # 下载方式一,太慢 # 下载方式二,推荐 2.解压到D:\Program Files (x86),这个看个人喜 ...

  3. Spring 梳理-数据访问-DB

    针对接口编程 DAO是指数据访问对象(data access object),它提供了数据读取和写入到数据库中的一种方式.Spring认为,它应该以接口的方式发布功能,而应用程序的其他部分需要通过接口 ...

  4. 阿里云服务器CentOS6.9安装SVN

    1.安装SVN yum -y install subversion 出现Complete表明安装成功 2.创建SVN仓库目录 mkdir -p /data/svn/repositories/yyksv ...

  5. CMD的最佳“代替品”

    让CMD成为历史 Windows用户大多都使用过"cmd",cmd被称为"阉割版"的DOS系统~ 很多用户除此之外,还喜欢Linux命令行~但是CMD的命令和L ...

  6. Python PDB调试处理

    pdb 是 python 自带的一个包,为 python 程序提供了一种交互的源代码调试功能,主要特性包括设置断点.单步调试.进入函数调试.查看当前代码.查看栈片段.动态改变变量的值等.pdb 提供了 ...

  7. TF-IDF算法——原理及实现

    TF-IDF算法是一种用于信息检索与数据挖掘的常用加权技术.TF的意思是词频(Term - frequency),IDF的意思是逆向文件频率(inverse Document frequency). ...

  8. Java读源码之Thread

    前言 JDK版本:1.8 阅读了Object的源码,wait和notify方法与线程联系紧密,而且多线程已经是必备知识,那保持习惯,就从多线程的源头Thread类开始读起吧.由于该类比较长,只读重要部 ...

  9. 神奇的Java僵尸(defunct)进程问题排查过程

    现象描述 大概1个月多以前 在启动脚本中增加了tail -f 用来启动后追踪日志判断是否启动成功 后发现无法执行shutdown.sh(卡住 利用curl) 然后无奈使用kill -9 但通过ps - ...

  10. sqlmap实战-1

    sqlmap实战-1 检测和利用sql注入 python2 sqlmap.py -u "[URL_SQL注入点]" [--batch] --batch:自动选择sqlmap默认的选 ...