【Python基础】05 组合数据类型

  数据类型是组织数据的方式,整数类型、浮点类型和复数类型等数字类型是基本数据类型,组合数据类型则是将基本数据类型组合起来,能更加有效地访问和处理这些数据,提高程序设计效率。在python中,组合数据类型主要包括序列类型、集合类型和映射类型,通过学习这些组合数据类型,当处理相应数据时便能选择合理的数据类型进行操作,从而满足实际需求。

更新历史:

  • 2021年06月20日完成初稿

1. 序列类型

  事实上,前面讲述的字符类型就是序列类型的一种,序列类型可以描述为具有先后次序关系的数据集合,这些元素在逻辑上可以认为是两两相邻的,在物理存储时往往也是相邻的。序列类型最大的特点就是可以通过下标(也称为索引)进行访问,因此可以实现随机访问。

访问类型
  一般而言,元素访问分为随机访问、顺序访问和索引访问:

  • 随机访问:可以根据元素位置来直接访问元素
  • 顺序访问:访问元素时需要从头至尾地(顺序地)寻找元素并进行访问
  • 索引访问:先访问元素的地址,再间接地访问元素

  在这里会涉及到随机访问和顺序访问,对于索引访问,在实现上它和指针有关,在这里不作具体讲述。

  常用的序列类型有字符串类型(str)、元组类型(tuple)和列表类型(list),字符串类型在前面介绍过,在下面就只介绍元组类型和列表类型。不过在介绍这些具体的数据类型之前,先介绍一下序列类型的共性,它们是所有序列类型都具有的特点。

1.1 序列类型概述

  正如上述,序列类型最大的特点就是可以通过下标进行访问,假设有一个序列S,其有n个元素,其元素的排列情况为:

  由于对于序列中的每一个元素,其都有一个唯一的下标,因此就可以通过下标访问元素,而且在python中还提供了正向序号和反向序号,为元素的访问特别是部分元素的访问提供了有力的手段。对于序列类型而言,常用的操作有切片、重复、连接和判断成员,下面针对这些操作一一进行介绍。

  通过下标可以访问某一位置的一个元素,而通过切片可以获取某一范围内的多个元素,其实通过指定元素的索引值范围来实现的,以熟悉的字符串类型s = "Hello, world!"为例(对元组、列表也是如此),其元素排列情况为:

  因此则有下面的切片操作:

# 字符串的切片操作
>>> s = "Hello, world!"
>>> s[1:5]              # 获得字符串a的第1位到第4位
'ello'
>>> s[-6:-1]            # 获得字符串a的第-6位到第-1位
'world'
>>> s[0:-1]             # 获得字符串a的第0位到第-2位
'Hello, world'
>>> s[::-1]             # 将字符串a逆序
'!dlrow ,olleH'

  上面的操作有些是容易理解的,对于切片s[a : b]而言,其表示获得字符串s的[a, b)位,不包含第b位,而且a和b即可以是正向序号,也可以是逆向序号(可以将逆向序号转换为正向序号进行理解)。更通用地,对于切片s[start : end : step]而言,则是从start开始,以步长step取字符,字符不超过end(不含end),而且start默认为0(或-1),end默认为n(或-n-1),step默认为1。因此对于上例的s[::-1]实际为s[-1:-14:-1],即为字符串逆序。

  切片提供了取序列元素的基本方法,在python中还提供了重复和连接的操作:

# 序列的重复和连接操作
>>> '5' * 5
'55555'
>>> (1, 2, 3) * 2
(1, 2, 3, 1, 2, 3)
>>> "Hello, " + "world!"
'Hello, world!'
>>> [1, 2] + [3]
[1, 2, 3]

  重复和连接操作十分的有用,需要注意的是,连接操作需要保证参与连接运算的两个序列是同一类型的,不同类型的序列不能完成连接操作。接下来介绍的就是序列中判断成员的运算符in和not in,这种操作在选择结构和循环结构中经常使用:

# 序列类型的判断成员运算符
>>> 'e' in "Hello, world!"      # 字符e在字符串"Hello, world!"中
True
>>> for c in "Hello, world!":
	print(c, end = '-')         # 打印字符c, 后接'-'(默认为换行)
H-e-l-l-o-,- -w-o-r-l-d-!-

  除了以上基本的操作以外,对于序列类型而言,还有一些内置的函数可以使用,下面就以表格的形式给出

函数 功能
len() 求序列的长度(元素个数),对之后的集合类型和字典类型也可使用
sorted(iter, key, reverse) 返回可迭代对象iter排序之后的列表,key是排序规则,reverse决定顺序或者逆序
reversed() 返回序列逆序排列的迭代器
sum(iter, start) 将可迭代对象iter中的数值和start参数(默认为0)相加
max(iter) 返回可迭代对象iter中的最大值
min(iter) 返回可迭代对象iter中的最小值
enumerate(iter, start) 返回一个迭代器对象,元素是参数iter元素的索引和值组成的元组,起始索引为start(默认为0)
zip(iter1[, iter2[…]]) 返回一个迭代器对象,元素是参数iter相同位置元素构成的元组

  下面就通过一些简单的例子来介绍上面的函数:

# 序列的内置函数
>>> lt = [3, 1 ,4, 1, 5, 9, 2, 6]    # 列表lt
>>> len(lt)                          # 列表长度
8
>>> ls = sorted(lt)                  # 对列表排序, 但原列表不变
>>> print(lt, ls)
[3, 1, 4, 1, 5, 9, 2, 6] [1, 1, 2, 3, 4, 5, 6, 9]
>>> list(reversed(lt))               # 对列表进行逆序, list()是列表的关键字
[6, 2, 9, 5, 1, 4, 1, 3]
>>> sum(lt, 1)                       # 对列表求和
32
>>> max(lt)                          # 求列表的最大值
9
>>> min(lt)                          # 求列表的最小值
1
>>> list(enumerate(lt))              # 产生enumerate对象
[(0, 3), (1, 1), (2, 4), (3, 1), (4, 5), (5, 9), (6, 2), (7, 6)]
>>> l = [1, 2, 3, 4, 5, 6, 7, 8]
>>> list(zip(l, lt))                 # 产生zip对象
[(1, 3), (2, 1), (3, 4), (4, 1), (5, 5), (6, 9), (7, 2), (8, 6)]

  这里值得注意的是sorted(lt)、reversed(lt)并不会改变原列表,而是产生新的列表或是对象,另外对于reversed(lt)、enumerate(lt)、enumerate(lt)并不产生一个列表而是一个迭代器,理解这个概念需要先理解可迭代对象和迭代器:

  • 可迭代对象:可以用于for循环等迭代过程的对象,如序列、迭代器等
  • 迭代器:拥有next方法的可迭代对象,通过next()方法可以访问迭代器当前元素

  对于字符串、元组和列表而言,它们是可迭代对象但不是迭代器,不过通过iter()方法可以将一个对象转换为迭代器,通过一些内置函数如reversed(lt)、enumerate(lt)、enumerate(lt)也可以产生一些迭代器,通过list()方法可以将其转换为列表类型。

可迭代对象
  通俗地说,可迭代对象就是一个大容器,这个容器可以容纳很多对象,而且通过for()循环可以对这些对象进行迭代,而迭代器就是容器中对象的名单(它也是一个可迭代对象),通过这个名单可以间接访问对象,但为了有条理只能按序访问这些元素,访问完了就不再访问了。关于迭代器的更多细节可参考python官方文档 Iterators

1.2 列表类型

  列表类型是序列类型的一种,因此列表也具上述的切片等操作和一些内置函数。特别地,列表是可变的容器对象,而且可以包含不同类型的元素。在python中,通过[]和list()方法可以创建一个列表:

# 创建列表
>>> lt = [2021053883, "Yang", "M"]  # 利用[]创建列表
>>> lt
[2021053883, 'Yang', 'M']
>>> ls = list("python")             # 利用list()将可迭代对象转换为元组
>>> ls
['p', 'y', 't', 'h', 'o', 'n']
>>> l = list(x for x in range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

  特殊地,列表本身也可以是一个元素,因此可以创建一个列表的列表,这很C语言的二维数组十分相似。对于列表类型,其有一些特殊的方法,下面一一进行介绍(下面用List代表列表对象):

方法 功能
List.append(x) 向列表尾部添加对象x
List.copy() 生成一个列表的拷贝
List.count(x) 返回x在列表中出现的次数
List.extend(t) 将可迭代对象t的每个元素添加到列表尾部
List.index(x, i, j) 返回对象x在列表中的所索引值,索引值范围在[i,j)之间
List.insert(i, x) 在列表中索引值为i的位置前插入对象x
List.pop(i) 删除索引值为i的列表对象,默认删除最后一个对象
List.remove(x) 删除第一个找到的对象x
List.reverse() 翻转列表(原列表会改变)
List.sort(key = None, reverse = False) 将列表排序(原列表会改变)

  在上面的方法中,有以下几个需要注意的地方

  • List.append(x)是将x整个地(当成一个元素)添加到列表尾部,而List.extend(t)要求t是一个可迭代对象,并将t中的所有元素加入列表
  • List.copy()是一种浅拷贝(List[:]也是浅拷贝的形式之一),即只复制父对象(一级元素)而不复制内部子对象(此时的内部子对象采用引用的形式),要想实现深拷贝,即既拷贝父对象也复制内部子对象,需要使用copy模块里面的deepcopy()函数。
  • 在上节讲到序列类型有内置函数reversed()和sorted(),这是对于所有序列类型都适用的,而且这些内置函数不会改变原序列,而List.reverse()和List.sort()是列表的方法,且会改变原列表。

  上面关于列表的拷贝论述比较多了,或者会有这样的疑问:为什么不直接使用赋值语句呢?原因在于通过List1 = List2会使得List1和List2指向相同的对象,对List2的改变也会对List1产生变化。

# 赋值与拷贝
>>> List1 = [1, 2, 3]
>>> List2 = List1       # 直接赋值
>>> List2[1] = 0        # 对List2的改变也会直接作用于List1
>>> List1
[1, 0, 3]

1.3 元组类型

  元组和列表十分相似,列表用方括号[]表示,而元组用圆括号()标识,不过两者主要的区别在于:列表是可变的,元组是不可变的。在python中,可以用圆括号()和tuple()方法来创建一个元组:

# 创建元组
>>> t = (2021053883, "Yang", "M")   # 利用()创建列表
>>> t
(2021053883, 'Yang', 'M')
>>> t = tuple("python")             # 利用tuple()将可迭代对象转换为元组
>>> t
('p', 'y', 't', 'h', 'o', 'n')
>>> 1, 2, 3                         # 未明确定义数据是列表还是元组等类型,默认为元组
(1, 2, 3)

  不过由于元组是不可变的,因此一旦创建一个元组就不能改变元组中元素的值,因此不能使用sort()和reverse()等方法改变元组,以至于元组没有sort()和reverse()方法。不过元组通常的用法是:

  • 作为映射类型中字典的键(key)
  • 作为函数的特殊类型的参数,即通过元组函数可以返回多个值

2. 集合类型

  对于集合而言,大家是不陌生的,有一定数学基础的读者对于集合的概念和操作是熟稔于心的。在python中,集合的概念也是和数学中的集合是一致的,集合是若干个不同元素的无序组合,因此集合中的任一元素都是不重复的,而且是没有次序的,因此和序列类型有着本质的不同。另外,集合的元素类型只能是不可变数据类型(整数类型、浮点数类型、字符串类型和元组类型等),而不能是可变数据类型(列表、字典等),但集合本身是一个可变的数据类型,只是不允许元素重复。

不可变集合
  在python中,其实可以声明可变集合,也可以声明不可变集合,不可变集合中元素不可以增加、删除和修改,只能进行查找等静态操作,用关键字 frozenset()创建。文章主要讲述的集合为可变集合,其应用更加广泛一些。

  可以通过花括号{}和set()方法创建一个集合:

# 创建集合
>>> s = {
    
    1, 2, 3, 3}
>>> s
{
    
    1, 2, 3}
>>> s = set("Hello")
>>> s
{
    
    'o', 'H', 'l', 'e'}   # 由于集合是无序的,因此其次序是任意的

  由于集合元素的无序性,因此不能对元素进行随机访问,只能顺序访问。但集合元素也是不允许重复的,因此可以利用集合轻松地完成去掉重复元素的任务,这是集合类型最主要的作用。

  对于集合而言,集合上的运算是十分重要的,和数学上的操作类似,元素与集合之间、集合和集合之间也有一些比较运算、关系运算等,如下表所示:

数学符号 python符号 含义
∈ \in in 判断元素是否是集合的成员
∉ \notin / not in 判断元素是否不是集合的成员
= = = == 判断集合是否相等
≠ \ne = != 判断集合是否不相等
⊂ \subset < 判断是否是集合的真子集
⊆ \subseteq <= 判断是否是集合的子集
⊃ \supset > 判断是否是集合的真超集
⊇ \supseteq >= 判断是否是集合的超集
∩ \cap & 返回两个集合的交
∪ \cup | 返回两个集合的并
\ \backslash \ − - 返回两个集合的差补
△ \bigtriangleup ^ 返回两个集合的对称差

  其中超集是和子集是相对应的概念,S1是S2的超集,则S2是S1的子集,而 \ \backslash \是差补运算,set1 - set2为出现在set1中但不出现在set2中的元素, △ \bigtriangleup 运算是对称差运算,其数学定义为 A △ B = ( A ∪ B ) \ ( A ∩ B ) A\bigtriangleup B=(A\cup B)\backslash (A\cap B) AB=(AB)\(AB)

  事实上,上面大多数运算符都有相对应的集合内建的函数或者方法:

函数/方法 功能
s.issubset(t) 判断集合t是否是s的子集
s.issuperset(t) 判断集合t是否是s的超集
s.union(t) 求集合s和t的并集(返回新集合)
s.intersection(t) 求集合s和t的交集(返回新集合)
s.difference(t) 差补运算(返回新集合)
s.symmetric_difference(t) 对称差运算(返回新集合)
s.copy() 返回集合s的副本(返回新集合)
s.update(t) 修改集合s,使集合s等于集合s和t的并
s.intersection_update(t) 修改集合s,使集合s等于集合s和t的交
s.difference_update(t) 修改集合s,使集合s等于集合s和t的差分
s.symmetric_difference_update(t) 修改集合s,使集合s等于集合s和t的对称差
s.add(obj) 将元素obj加入集合s中
s.remove(obj) 将元素obj从集合s中删除,若s不含obj则产生KeyError异常
s.discard(obj) 将元素obj加入集合s中,若不存在,不返回异常
s.pop() 从集合s中删除任意一个元素,并返回这个元素
s.clear() 将集合s的元素清空

3. 映射类型

  映射类型是键值对的无序组合,其每一个元素都是键值对,键(key)描述了对象的属性,而值(value)是属性的具体内容,这种数据结构在生活中十分的常用,比如学生成绩管理系统中,名字就是一个键而成绩就是键的值,两者是一一对应的。而在python中,映射类型主要由字典体现,下面简单介绍一下字典的概念和使用。

  字典是python中的一种独特的数据结构,其建立了对象之间的映射关系,字典在建立过程中,形成了键(key)和值(value)之间的关系,形成了key-value对,通过对于键的索引就可以确定值。字典可以通过花括号{}和dict()方法创建:

# 创建字典
>>> dict1 = {
    
    'ZhangSan':90, 'LiSi':88, 'WangWu':90}
>>> dict1
{
    
    'ZhangSan': 90, 'LiSi': 88, 'WangWu': 90}
>>> stu = (('ZhangSan', 90), ('LiSi', 88), ('WangWu', 90))
>>> dict2 = dict(stu)
>>> dict2
{
    
    'ZhangSan': 90, 'LiSi': 88, 'WangWu': 90}
>>> 

字典和集合
  可以看到通过字典和集合都是通过{}来创建的,不过对于a = {}而言,其实际上创建了一个空的字典而不是一个集合,这一点需要注意,空的集合需要通过a = set()来创建。

  字典最大的特点就是存在键值对(key-value),通过查找相应的key就可以查找到对应的value,可以通俗地将key作为value的索引,看看下面的例子:

# 字典的索引
>>> dict1 = {
    
    'ZhangSan':90, 'LiSi':88, 'WangWu':90}
>>> dict1['LiSi']        			# 通过key查找value
88
>>> dict1['LiSi'] = 100  			# 通过key修改value
>>> dict1['Yang'] = 95   			# 若key不存在,则添加key
>>> dict1
{
    
    'ZhangSan': 90, 'LiSi': 100, 'WangWu': 90, 'Yang': 95}

  在字典中还有一些常见的函数或者方法,下面一一介绍(用D表示字典):

函数/方法 功能
D.keys() 返回字典D的键的列表
D.values() 返回字典D的值的列表
D.items 返回字典D的键值对构成的列表
D.get(key, default = None) 返回键key对应的值,若不存在,则返回default值
D.copy() 返回D的副本
D.pop(key[, default]) 将该键值对返回并从字典中删除
D.clear() 清空字典
D.update(dict2) 将字典dict2中的键值对添加到D中,若键已经存在,则更新键对应的值
D.fromkeys(seq[, value]) 创建并返回字典

  在上面,介绍了序列类型、集合类型和字典类型,有必要在最后做一个总结:

  序列类型是一种有序的数据类型,可以通过下标来访问数据元素,同时数据元素可以是同类型的、不同类型的,甚至可以是重复的。而集合类型是不同数据元素的无序集合,字典类型是有键值对关系的"集合“。当需要组织不同数据类型的元素时,可以考虑列表或者元组,列表是可变的,而元组是不可变的。如果数据元素不允许重复,则集合是一个合理的选择,而当处理的数据存在映射关系时,则毫无疑问地应该选择字典。

  总而言之,现在我们拥有了组织数据的能力,也掌握了处理数据的函数或者方法,对于一般数量的数据处理起来是游刃有余的,但当需要处理几百万个字符的数据时(如《红楼梦》),这个时候选择任一种数据结构就有点捉襟见肘了,这时候就需要通过文件来处理了。

猜你喜欢

转载自blog.csdn.net/Stu_YangPeng/article/details/118072735