数据类型是组织数据的方式,整数类型、浮点类型和复数类型等数字类型是基本数据类型,组合数据类型则是将基本数据类型组合起来,能更加有效地访问和处理这些数据,提高程序设计效率。在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) A△B=(A∪B)\(A∩B)。
事实上,上面大多数运算符都有相对应的集合内建的函数或者方法:
函数/方法 | 功能 |
---|---|
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]) | 创建并返回字典 |
在上面,介绍了序列类型、集合类型和字典类型,有必要在最后做一个总结:
序列类型是一种有序的数据类型,可以通过下标来访问数据元素,同时数据元素可以是同类型的、不同类型的,甚至可以是重复的。而集合类型是不同数据元素的无序集合,字典类型是有键值对关系的"集合“。当需要组织不同数据类型的元素时,可以考虑列表或者元组,列表是可变的,而元组是不可变的。如果数据元素不允许重复,则集合是一个合理的选择,而当处理的数据存在映射关系时,则毫无疑问地应该选择字典。
总而言之,现在我们拥有了组织数据的能力,也掌握了处理数据的函数或者方法,对于一般数量的数据处理起来是游刃有余的,但当需要处理几百万个字符的数据时(如《红楼梦》),这个时候选择任一种数据结构就有点捉襟见肘了,这时候就需要通过文件来处理了。