4、数据结构元祖和bytes
元祖(不可变类型)
元素访问
元祖方法
命名元祖
bytes(不可变数据结构)
bytes与字符串
bytes与编码
bytes与字符串的转换
编码表
bytes定义
bytearray(可变数据结构)
切片
切片索引方式
切取单个值
切取完整对象
切取奇偶数
切片进阶操作
均为正整数的情况
均为负整数的情况
混合索引的情况
元素访问
元祖方法
命名元祖
bytes(不可变数据结构)
bytes与字符串
bytes与编码
bytes与字符串的转换
编码表
bytes定义
bytearray(可变数据结构)
切片
切片索引方式
切取单个值
切取完整对象
切取奇偶数
切片进阶操作
均为正整数的情况
均为负整数的情况
混合索引的情况
元祖(不可变类型)
元祖Tuple也是有序的元素组成的集合类型,使用()表示,并且元祖是不可变的,当元祖只有一个元素的时候,必须以逗号结尾,并且元祖和列表差不多,内部的元素可以是任何合法的数据类型;
Python的元组与列表类似,不同之处在于元组的元素不能修改,元组使用小括号,列表使用方括号,元组创建很简单,只需要在括号中添加元素,并使用逗号隔开即可;
虽然说元祖不可修改,这一点没毛病,但是如果元祖内部含有一个可变的数据类型,我们是可以修改的,因为这个元素只是一个在这个元祖里面只是一个引用;
从数据结构上来讲,元祖比列表设计得比较轻巧,因为它舍弃掉很多的东西,所以在有的时候,我们明确的知道元素不会进行改变,那么这个时候就可以直接使用tuple,也可以防止被修改,类似于常量;
元素访问
Tuple元素的访问和List的一样的,索引不可超界,超界会引发IndexError;
正索引:从左至右,从0开始;
负索引:从右至左,从-1开始;
元祖方法
因为元祖不可变的特性,它和列表不同,列表所有可修改新增类操作元祖都没有,但是其他的是有的;
index(value,[start[stop]]):通过value,从指定区间查找元素是否匹配,返回一个匹配后元素的索引号,其中可选参数start和stop,为起始区和结束区间;
count(value):返回列表中匹配value的次数;
命名元祖
命名元祖,即可以命名的元祖,它实际上是一个继承于Tuple定义的类,即namedtuple,它用元祖作为父类的原因是,元祖元祖有一个好处在于,元祖的元素一旦定义了,就不可变,所以说命名元祖也是一样,一旦定义好了,就不允许改变,namedtuple本身是一个tuple的子类,使用namedtuple可以创建出一个新类,这个新类是tuple的子类,然后我们用这个新类就可以实例化出几个不可变的对象出来;
namedtuple接受两个参数,第一个参数是生成的新类的类名,第二个参数为这个类有的数据属性的名称,当创建好新类之后,我们就可以对这个类进行实例化,实例化成一个具有tuple特性对对象;
from collections import namedtuple
People = namedtuple('P', ['name','age'])
# Point为一个接受新类的标识符,换而言之,就是Point指向了新类在内存中的地址;
# P为新类的名称,也就是通过namedtuple创建出来的类的类名;
# 'x,y'则是创建出的新类的数据属性的名称;
p1=People("cce",18) # 实例化创建出的新类
print(type(p1)) # <class '__main__.P'> 打印p1对象所属的类
print(isinstance(p1,People)) # True 可以看到p1就是People类的实例,其实People就是P
print(p1) # P(name='cce', age=18) 仅用于显示该实例具有哪些数据属性
print(p1.name) # cce
print(p1.age) # 18
bytes(不可变数据结构)
bytes从名称上直接理解的话就上字节,bytes指的其实就上在内存中连续排放的字节序列,它主要是解决字符串的问题,因为字符串是字符序列,也是在内存中连续排放的字符序列,因为字符串是字符序列,那么使用字符串去描述类型中文这种数据类型的时候,实际上就是用的多字节序列,那么多字节序列就不能用一个字节来描述,所以它就是一个字符序列;
本质上来看计算机里面放置都是0和1,一个0或者1都是一个位,每8位都是一个字节,所有东西都可以当字节序列理解,这没问题,但是人不容易理解,人在使用字符的时候,希望的是一个个字符,这样实际上就跟字节序列就有了差别;
那么对于计算机的字符序列,计算机不会管到底是字符还是什么,因为计算机都是一个字节一个字节理解的,不管是字符串还是整形,比如我们要用计算机表示一个十进制60000,那我们只能用多个字节,因为一个字节描述不了,两个字节就可以描述了,所以这个时候,我们往往使用的都是多字节序列;
那么对于字符串在计算机中存储也需要多个字节,所以多个字节,我们是必须使用到的,如果在内存中,不加区分,计算机根本不知道到底是数字还是字符串,因为在内存中都是0和1,所以说,为什么高级语言有数据类型呢,内存中都是0和1,我们只有按照某种数据类型去理解它,它才有这个意思了,否则的话,它就是简单的0和1组成的字节而已;
所以在Python3中提供了一种字节序列的用法,字节序列分两种一种是不可变的bytes字节序列和可变的bytearray字节数组,bytearray其实和列表一样;
bytes与字符串
字符串是字符组成的有序序列,那么字符序列放在内存中还是得有一个一个字节的0和1组成,也就是说还是得由一个字节组成,说到底还是bytes,但是有字符就得有编码;
bytes与编码
编码是为字符串服务的,编码的作用就是,在计算机中拿几个字节,去理解这个数值,拿10个字节还是拿2个字节去理解这个数据,那么bytes呢,就不管是什么值,整数也罢、字符串也罢哪怕是个列表也罢,反正对于bytes来说,都是0和1组成的,8位8位的连续的字节序列;
bytes与字符串的转换
比如说我们有个字符串,要得到它的bytes那么直接使用encode方法即可获取到,并且在内部我们可以传入使用什么编码将字符串转换为bytes,python3默认使用的编码是utf8类型,最终得到了一个b开头的bytes值,这个b只是显示的告诉我们,它是一个bytes,bytes是不可变的,一旦定义好了就是一个常量;
那么我们希望将一个bytes转换为字符串,使用decode即可,即可返回一个当前语言的合法数据类型,一般是字符串,这种转来转去的操作,在互联网上大量的使用;
print("蔡大爷".encode(encoding="utf8")) # b'\xe8\x94\xa1\xe5\xa4\xa7\xe7\x88\xb7'
print("蔡大爷".encode(encoding="gbk")) # b'\xb2\xcc\xb4\xf3\xd2\xaf'
print(b'\xb2\xcc\xb4\xf3\xd2\xaf'.decode("gbk")) # 蔡大爷
编码表
最经典的编码也就是ASCII,ASCII码表如下,称之为美国信息交换表,ASCII码表刚开始是一个单字节编码表,因为计算机里面放的只能是0和1,0和1天生就是用来描述数据的,那么这个时候遇到ABC这样字母就犯难了;
因为数字在计算机中天生可以使用0和1表示,这个没问题,那么对于ABC这样的字符呢,就无法表示了,所以有人就写了一张表,也就是码ASCII表,用一个数字代表一个字符,所以就直接将A-Za-z加入了这个表,也就是下面的表,就拿一个数字代指一个字符,比如9就是\t,A就用41表示,只要我们在内存里面放一个十六进制的41就代表A,十进制的41那么就是),当然,前提是我们需要告诉它是什么表;
表是用来查的,如果放置的不是数字,而是字符串,就需要用这个数字查询这种表,也就是说我们无法去描述字符,所以就建立了一张编码表,然后我们将数据存储到内存时,因为内存天生就支持整形类型,我们就用它天生支持的数字告诉它,数据是字符串就需要去查表,比如这个数字就是13,那么得到的结果就是回车符;
那现在突然有一个要求了,我们想要描述一个A,那么我们就需要将这个有A的编码表给它建立出来,所以说这样一张ASCII码表就建立出来了,它表示了128种类型也就是10000000,因为单个字节能够描述256个状态,也就是11111111,从0到255,所以前128个状态用来描述ACSII码表,后面那个128-255也就是01111111就用来设计了扩展ASCII码表,但是扩展ASCII码表就不是谁说了算了,比如欧洲某些国家,就把这块重新定义了;
那也就是说,内存中的一个数字,我们想把它转换成一个字符,我们就得告诉它使用哪张表,因为不同的表的扩展表,实现了不同的数字和映射,比如一个我们自己定义的表,将十进制41换成了B,如果我们不指定使用哪张表的话,那么我们就无法将这个41转换成B,所以说内存中一个字节,我们想要把它对应到一个字符上去,我们就需要明确的告诉计算机使用哪张编码表;
编码实现的是内存中的数字与字符的对应关系,但是编码表太多了,每个国家都可以根据字节的实际情况,然后定义自己的编码表,但是呢,大多数在设计编码表的时候,绝大多数都兼容ASCII码表,也就是说,大多数的编码表的0-127全部保留不动,留给ASCII码表,我们现在看到的编码类型,也全部都将0-127都留给了标准的ASCII码表,然后128-255就各有不同了;
然后由于中文的表示方法128-255是不够用的,所以就引入了双字节,这样的话中国就引入了一个编码表,GB2312,后来由于GB2312对繁体字并不支持,所以对其进行了扩展,也就是GBK,又称GBK大字符集,简而言之就是将所有亚洲文字的双字节字符,包括简体中文,繁体中文,日语,韩语等,都使用一种格式编码,兼容所有平台的上的语言。GBK大字符集包含的汉字数量比GB2312和BIG5多,使得汉字兼容足够使用;
由于各个国家使用的表不一样,那么这就带来问题了,如果我们除了像ASCII码表以外,我们既想使用中文又想日文的话,那就有问题了,因为一般一篇文章我们只能指定一个编码格式,所以一旦我们指定了一种编码, 那么这篇文章所有的字符都将使用指定的这张编码表,我们想在里面描述一个日文,是无法做到的;
所以当时为了方便,我们中文编码的时候,像ASCII码一样,留下了一段区域,可以用于扩展,但是我们也不能把全球的语言都包含,那么这个时候, 世界上就出现一种标准编码,全球编码,这个编码就是unicode,统一了全球的编码,将全球的所有文字,全部涵盖在内,利用2个字节来描述全球编码;
utf-8是unicode之后衍生出来的编码方式,这种编码方式是一种多字节的编码方式,它和unicode之间会做一次转换,utf-8可以将unicode字符转换成1-6个字节,目前我们常看到的转换是1-3个字节,中文大多数转换完之后是3个字节,目前最主流的也就是utf-8,unicode虽然兼容来ASCII码表,但是它是双字节的,如果我们要表示字母A,用unicode表示就是\x41,\x表示是十六进制,41表示对应的数,用2个字节来描述A,会浪费空间,而utf-8用来描述字符的,A只会使用一个字节,说白了utf-8会根据不同的语言来进行不同长度大小的存储;
我们为了解决内存中的数据能够描述字符,我们必须将内存中的数据给它设定一个类型,这个类型往往是一个字符串,并且还需要给定一个编码类型,如果说我们要做的是国际化的,全球通用的,这个时候,往往我们得选择unicode或者utf-8,因为他们是全球编码的,全球各个地方的语言都能够解码,编码说到底就是如何理解内存中的数据;
bytes定义
bytes一旦定义,就不允许更改;
print(bytes()) # b''
print(b"") # b''
print("".encode()) # b''
# 创建长度为10的bytes,用0填充
print(bytes(10)) # b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
# 查询十进制对应的字符
print(bytes([13])) # \r
bytearray(可变数据结构)
bytearray是可变的,类似列表,列表有的方法,bytearray也有,只不过在插入修改等操作的时候,需要传入十进制数值或者十六进制ASCII码的表示法;
切片
利用python解决问题的过程中,经常会遇到从某个对象中抽取部分值的情况。“切片”操作正是专门用于实现这一目标的有力武器。理论上,只要条件表达式得当,可以通过单次或多次切片操作实现任意目标值切取。切片操作的基本语法比较简单,但如果不彻底搞清楚内在逻辑,也极容易产生错误,而且这种错误有时隐蔽得较深,难以察觉。本文通过详细例子总结归纳了切片操作的各种情形,下文均以list类型作为实验对象,其结论可推广至其他可切片对象;
切片索引方式
如图所示,这就是Python的切片索引方式,它包括正负索引两部分;
一个完整的切片,应该包含":",用于分割三个参数(start_index,stop_index,step),当只有一个":"时,默认第三个参数,step为1,当一个":"都没有时,start_index=end_index,表示切取start_index指定的那个元素;
step : 正负数均可,其绝对值大小决定了切取数据时的"步长",而正负号决定了"切取方向",正表示"从左往右"取值,负表示"从右往左"取值。当step省略时,默认为1,即从左往右以步长1取值,切取方向非常重要!
start_index : 表示起始索引,该参数省略时,表示从对象"端点"开始取值,至于是从"起点"还是从"终点"开始,则由step参数的正负决定,step为正从"起点"开始,为负从"终点"开始。
end_index : 表示终止索引,该参数省略时,表示一直取到数据"端点",至于是到"起点"还是到"终点",同样由step参数的正负决定,step为正时直到"终点",为负时直到"起点"。
切片在日常开发中也是使用较为频繁的操作,其中针对切片来说,个人总结三大原则,如下;
原则一:索引为0开始,并非为1开始;
原则二:step意为步进,默认情况步进为正整数1,当step为负整数-1时,从右往左取值,是取值,并非反转;
原则三:当start_index为空,表示从起始开始取,至于这个起始是开头还是结尾,主要是看step是正数还是负数、stop_index为空时,就表示直接取到此方向的终点,并非取到0;
切取单个值
切取单个值,只需要在start_index、stop_index这两个参数中,任意给定一个参数即可,遵循上面的正负原则;
container = [1, 2, 3, 4, 5, 6]
# 正负都有不同的表示,正数表示从起点开始,负数表示从终点开始
print(container[1], container[-1]) # 2 6 索引是从0开始的,所以正数1表示容器里面的第二个数
切取完整对象
切取完整对象分多种情况,给定一个或者两个":"表示切取从头到尾所有的值,另外,step默认为1,当step为-1时,那么将返回一个倒序排列的结果;
container = [1, 2, 3, 4, 5, 6]
# 给定一个":",即切取从开头到结尾所有元素
print(container[:]) # [1, 2, 3, 4, 5, 6]
# 两个"::"也是一样的,也表示切取从开头到结尾所有元素
print(container[::]) # [1, 2, 3, 4, 5, 6]
# 因为step默认为正整数1,那么当step为负数时,表示从右往左取值,并且step为step的值,如下step为-1,即容器倒序,如果为-2,那么倒序排列,并且步进为2
print(container[::-1]) # [6, 5, 4, 3, 2, 1]
# 浅拷贝
print(container.copy()) # [1, 2, 3, 4, 5, 6]
切取奇偶数
我们也可以利用step步进这个参数来切取奇偶数,因为step表示的就是当取出第一个值的时候,切取下一个值需要距离现在值的索引距离是几。
# 取奇数
print(container[::2]) # [1, 3, 5]
# 取偶数
print(container[1::2]) # [2, 4, 6]
切片进阶操作
对于进阶操作,可能比较绕,涉及到正数、负数针对三个参数的轮换,较为复杂,但是,但是先要知道一点,就是step就两种变化,正数即正序步进,负数即列表反转加步进,如下示例;
container = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
均为正整数的情况
均为正数,即start_index、stop_index和step三个参数,都是正数的情况,正常逻辑取之即可,直接从左到右取值,如下;
# 从左往右包前不包后
print(container[1:5]) # [1, 2, 3, 4]
# step=-1,决定了从右往左取值,而start_index=1到end_index=6决定了从左往右取值,两者矛盾,所以为空。
print(container[1:5:-1]) # [] 输出为空列表,说明没取到数据
# step默认等于正数1,那么step=1,决定了从左往右取值,而start_index=6到end_index=2决定了从右往左取值,两者矛盾,所以为空。
print(container[6:2]) # [] 同样输出为空列表。
# step=1,表示从左往右取值,而start_index省略时,表示从端点开始,因此这里的端点是“起点”,即从“起点”值0开始一直取到end_index=10(该点不包括),即使end_index超出索引,也无碍。
print(container[:10]) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# step=-1,从右往左取值,而start_index省略时,表示从端点开始,因此这里的端点是“终点”,即从“终点”值9开始一直取到end_index=6(该点不包括)。
print(container[:6:-1]) # [9, 8, 7]
# step=1,从左往右取值,从start_index=6开始,一直取到“终点”值9。
print(container[6:]) # [6, 7, 8, 9]
# step=-1,从右往左取值,从start_index=6开始,一直取到“起点”0。
print(container[6::-1]) # [6, 5, 4, 3, 2, 1, 0]
均为负整数的情况
均为负整数,即start_index、stop_index和step三个参数,都是负数的情况,一旦加入负数就稍微复杂起来了,因为存在反向取值,从右到左取值,如下;
# step=1,从左往右取值,而start_index=-1到end_index=-6决定了从右往左取值,两者矛盾,所以为空。
print(container[-1:-6]) # []
# step=-1,从右往左取值,start_index=-1到end_index=-6同样是从右往左取值。
print(container[-1:-6:-1]) # [9, 8, 7, 6, 5]
# step=1,从左往右取值,而start_index=-6到end_index=-1同样是从左往右取值。
print(container[-6:-1]) # [4, 5, 6, 7, 8]
# step=1,从左往右取值,从“起点”开始一直取到end_index=-6(该点不包括)。
print(container[:-6]) # [0, 1, 2, 3]
# step=-1,从右往左取值,从“终点”开始一直取到end_index=-6(该点不包括)。
print(container[:-6:-1]) # [9, 8, 7, 6, 5]
# step=1,从左往右取值,从start_index=-6开始,一直取到“终点”。
print(container[-6:]) # [4, 5, 6, 7, 8, 9]
# step=-1,从右往左取值,从start_index=-6开始,一直取到“起点”。
print(container[-6::-1]) # [4, 3, 2, 1, 0]
混合索引的情况
混合索引,即start_index、stop_index和step三个参数,同时存在负数和正数的情况,这种情况可能更加的复杂,如下;
# start_index=1在end_index=-6的左边,因此从左往右取值,而step=1同样决定了从左往右取值,因此结果正确
print(container[1:-6]) # [1, 2, 3]
# start_index=1在end_index=-6的左边,因此从左往右取值,但step=-则决定了从右往左取值,两者矛盾,因此为空。
print(container[1:-6:-1]) # []
# start_index=-1在end_index=6的右边,因此从右往左取值,但step=1则决定了从左往右取值,两者矛盾,因此为空。
print(container[-1:6]) # []
# start_index=-1在end_index=6的右边,因此从右往左取值,而step=-1同样决定了从右往左取值,因此结果正确。
print(container[-1:6:-1]) # [9, 8, 7]