网站首页 > 基础教程 正文
关注公众号:用Python学机器学习,获取更多更新。
序列首先是一种数据存储方式,用来存储一系列的数据。序列存储数据的主要特点就是数据在内存空间中是连续存储的,例如字符串abc(字符串属于序列),其在内存中的存储可以表达如下:
基于这一存储特点,Python中实现了序列类型。序列类型与很多Python内置数据类型都有密切的关系,掌握好序列类型将对理解Python内置数据类型有很大的帮助,这篇文章我们对序列类型做一个简要介绍。
序列类型的分类
Python中有很多数据类型都可以称为是序列类型,例如字符串str、列表list、元祖tuple、字节对象bytes、数组array.array、内存视图memoryview、字节数组bytearray等等。序列有很多分类标准,根据序列中存储的是值还是引用,序列可以分为扁平序列和容器序列。根据序列是否可以被修改,可以分为不可变序列(Sequence)和可变序列(MutableSequence)。
扁平序列
扁平序列有两个特点,第一,内部存储的都是值而不是引用(或者说是内存地址);第二,内部存储的都是同一种数据类型,而且只能存储数值、字节、字符这样的基础数据类型。常见的扁平序列如字符串str、字节bytes、数组array.array、字节数组bytearray和内存视图memoryview等。
我们举一个例子,例如a = 'abc',这是一个创建字符序列str的命令。这条命令运行时,Python解释器会先在内存中开辟一块连续的内存空间来存储a、b和c三个字符,创建好对象后会将这块内存的首地址抛给外界,由变量a来接收,变量a是另一块内存,这块内存中就存储了字符串序列'abc'的内存首地址。之后如果需要使用'abc'这个对象,都是通过变量a。当解释器读到变量a时,发现其存储的是一个内存地址,就会直接读取这个内存地址对应数据,完成对象的访问。如下图所示:
容器序列
与扁平序列相对应,容器序列中存储的不是值而是对象的引用,正因为如此,容器中可以容纳任何数据类型。常见的容器序列包括列表list、元祖tuple等。
如下例子:a = [1,'a',[5.6,'n']],这是一个列表,其在内存中存储方式如下图所示:
可以看到,对于列表本身来说,其在内存中是连续的,但是列表的内存中存储的并不是值本身,而是对象(列表中元素)的引用。至于对象(列表中的元素)本身,则存储在别的内存块中,这些内存可能是连续的,也可能是不连续的,大概率是不连续的。而且,列表中不仅仅可以存储基本数据类型int、str,还可以存储列表list。
可变序列
可变序列是指可以在原内存地址上对数据进行修改的序列。这类序列包括列表list、字节数组bytearray、数组array.array,内存视图memoryview等。
列表list提供了append方法,可以在原列表内存地址上对列表进行修改:
>>> b = [1,2,3] >>> id(b) 1665411984704 >>> b.append(6) >>> id(b) 1665412212544 >>> b [1,2,3,6]
这里给列表增加了一个元素,但是列表的内存地址并没有发生变化。
不可变序列
不可变序列指的是不可以在原内存地址上对序列进行修改。这类序列包括字符序列str、元祖tuple、和字节序列bytes。
例如,对于字符序列str,一旦创建就无法在原内存地址上对数据进行修改,强行修改则是创建新的对象:
>>> a = '123' >>> id(a) 1665411984752 >>> a = 'anc' >>> id(a) 1665411959088
可能有人对元祖不可修改无法理解,对于容器序列来说,不可变指的是容器中每一个元素的引用不可变,而不是每一个元素的值不可变。如下面的例子:
>>> a = ('1','2',['3','4']) # 记录元祖的内存地址 >>> id(a) 1665412226688 # 记录元祖中第一个元素的内存地址 >>> id(a[0]) 140710344787616 # 修改元祖中的第一个元素,可以看到报错了,提示元祖对象不支持赋值。因为第一个元素是一个不可变对象,强行修改会创建新的对象,产生新的引用,而元祖不支持修改内部元素的引用,所以报错。 >>> a[0]='5' Traceback (most recent call last): File "<pyshell#14>", line 1, in <module> a[0]='5' TypeError: 'tuple' object does not support item assignment # 记录元祖中第三个元素列表的内存地址 >>> id(a[2]) 1665412212544 # 我们的元祖中第三个元素是一个list,这是一个可变的序列,使用append方法会在原内存地址上进行修改,这样保证元素中第三个元素的引用并不会发生变化,所以修改成功。 >>> a[2].append('9') # 可以看到第三个元素列表修改成功 >>> a (1, 2, [3, 4, 9]) # 但是第三个元素的内存地址并未发生变化 >>> id(a[2]) 1665412212544 # 元祖第三个元素修改后,元祖本身的内存地址并未发生变化。 >>> id(a) 1665412226688
这个例子中,我们先创建了一个元祖(1,2,[3,4]),元祖中有三个元素1、2、[3,4],前两个都是不可变的字符序列str,通过前面的例子我们已经知道,如果对字符序列强行修改,不会改变原来的字符,而是创建新的对象。新的对象就意味着产生一个新的内存地址,这会导致元祖第一个元素的引用发生变化。这是元祖不能够接受的,所以出现报错。
然而,我们发现对创建的元祖的第三个元素进行修改,却修改成功了,原因是第三个元素是一个列表list,是一个可变序列,对其调用append方法是在原地址上对数据进行的修改,而并不会改变本身的内存地址,因此元祖第三个元素的引用不会发生变化,故而修改成功。
因此我们说对于容器序列,不可变意味着元素的引用不可变,相反,可变则意味着引用可以发生变化:
>>> a = [1,2,[3,4]] >>> id(a) 2338191916032 >>> id(a[2]) 2338200822784 >>> a[2]=5 >>> a [1, 2, 5] >>> id(a[2]) 140710407702304 >>> id(a) 2338191916032
看见列表是可以修改元素的引用的,因为它是可变类型。
序列类型的协议
以上我们对Python中的序列类型进行了分类,接下来我们学习一下序列类型的协议。通过这一部分的学习,你会对面向对象以及常见序列类型有更加深刻的认识。
Python为可变序列和不可变序列提供了两个基类Sequence和MutableSequence,这两个基类存在于内置模块collections.abc中,与其他常见的类如int、list等不同,这两个基类都是抽象基类。这里涉及到一个新的概念抽象基类,什么是抽象基类呢?
对于抽象基类,目前可以不用关注太多,只需知道抽象基类是指不能实例化产生实例对象的类,后面我们会专用一篇文章来介绍抽象基类。
Sequence和MutableSequence是两个抽象基类,如果不能实例化产生实例对象,那你可能要问了,那要Sequence和MutableSequence两个抽象基类还有什么作用呢?
其实抽象基类的作用并不是实例化产生实例对象的,它的作用更多的像是定义一种规则,或者官方的说法叫做协议,这样以后我们希望创建这种类型的对象时,要求遵循这种规则或者协议。现在我们需要了解序列类型都有哪些协议,这需要学习abc模块中的Sequence和MutableSequence两个类。
Sequence和MutableSequence两个类的继承关系如下:
图中粗体表示抽象基类,斜体表示抽象方法(抽象方法先有一个印象即可,目前可以先理解为并未做具体实现的方法,后续会专门写一篇文章来介绍抽象方法),剩下的为抽象基类中已经实现的方法。
可以看到,这里面的继承关系并不复杂,但是信息量很大,应该牢记这个图,因为这对理解序列类型非常重要。我们看到,可变序列MutableSequence类继承自不可变序列Sequence类,Sequence类又继承了两个类Reversible和Collection,Collection又继承自Container、Iterable、Sized三个抽象基类。通过这个继承图,我们至少应该能够知道,对于标准不可变序列类型Sequence,应该至少实现以下几种方法(遵循这些协议):
__contains__,__iter__,__len__,__reversed__,__getitem__,index,count
这几个方法到底意味着什么呢?这里以Python的内置类型list为例进行说明:
实现了__contains__方法,就意味着list可以进行成员运算,即使用in和not in;
实现了__iter__方法,意味着list是一个可迭代对象,可以进行for循环、拆包、生成器表达式等多种运算;
实现了__len__方法,意味着可以使用内置函数len()。同时,当判断一个list的布尔值时,如果list没有实现__bool__方法,也会尝试调用__len__方法;
实现了__reversed__方法,意味着可以实现反转操作;
实现了__getitem__方法,意味着可以进行索引和切片操作;
实现了index和count方法,则表示可以按条件取索引和统计频数。
标准的Sequence类型声明了上述方法,这意味着继承自Sequence的子类,其实例化产生的对象将是一个可迭代对象、可以使用for循环、拆包、生成器表达式、in、not in、索引、切片、翻转等等很多操作。这同时也表明,如果我们说一个对象是不可变序列时,暗示这个对象是一个可迭代对象、可以使用for循环、......。
而对于标准可变序列MutableSequence,我们发现,除了要实现不可变序列中几种方法之外,至少还需要实现如下几个方法(遵循这些协议):
__setitem__,__delitem__,insert,append,extend,pop,remove,__iadd__
这几个方法又意味着什么呢?同样以Python的内置类型list为例进行说明:
实现了__setitem__方法,就可以对列表中的元素进行修改,如a = [1,2],代码a[0]=2就是在调用这个方法
实现了__delitem__,pop,remove方法,就可以对列表中的元素进行删除,如a = [1,2],代码del a[0]就是在调用__delitem__方法
实现了insert,append,extend方法,就可以在序列中插入元素;
实现了__iadd__方法,列表就可以进行增量赋值。
这就是说,对于标准可变序列类型,除了执行不可变类型的查询操作之外,其子类的实例对象都可以执行增删改的操作。
鸭子类型
抽象基类Sequence和MutableSequence声明了对于一个序列类型应该实现那些方法,很显然,如果一个类直接继承自Sequence类,内部也重载了Sequence中的七个方法,那么显然这个类一定是序列类型了,MutableSequence的子类也是一样。确实如此,但是当我们查看列表list、字符序列str、元组tuple的继承链时,发现在其mro列表中并没有Sequence和MutableSequence类,也就是说,这些内置类型并没有直接继承自这两个抽象基类,那么为什么我们在文章的开头还要说他们都是序列类型呢?
>>> list.__mro__ (<class 'list'>, <class 'object'>) >>> tuple.__mro__ (<class 'tuple'>, <class 'object'>) >>> str.__mro__ (<class 'str'>, <class 'object'>)
其实,Python中有一种被称为“鸭子类型”的编程风格。在这种风格下,我们并不太关注一个对象的类型是什么,它继承自那个类型,而是关注他能实现那些功能,定义了那些方法。正所谓如果一个东西看起来像鸭子,走起来像鸭子,叫起来像鸭子,那他就是鸭子。
在这种思想之下,如果一个类并不是直接继承自Sequence,但是内部却实现了__contains__,__iter__,__len__,__reversed__,__getitem__,index,count几个方法,我们就可以称之为不可变序列。甚至都不必这么严格,可能只需要实现__len__,__getitem__两个方法就可以称作是不可变序列类型。对于可变序列也同样如此。
鸭子类型的思想贯穿了Python面向对象编程的始终,现在如果不理解没有关系。下一篇文章,我么将介绍不可变序列str,我们会对str和Sequence进行对比,到时候相信大家就能明白。
- 上一篇: Python的序列
- 下一篇: Python学习笔记:序列
猜你喜欢
- 2024-11-27 Python中的bytearray对象,灵活的字节序列处理
- 2024-11-27 学习编程第141天 python序列解包特性及其便捷赋值
- 2024-11-27 学习编程第159天 python编程 序列的增删改查
- 2024-11-27 Python中的时间序列分解
- 2024-11-27 Python入门第4课:字符串、序列
- 2024-11-27 python基础 数值和序列类型
- 2024-11-27 学习编程第158天 python编程 利用序列求最值和存在性
- 2024-11-27 python每天学习一点点(判断数元素是否在序列中的2种方法。)
- 2024-11-27 Python中6种内建序列之通用操作
- 2024-11-27 通过 Python 代码实现时间序列数据的统计学预测模型
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)