Python 语言功能强大的重要原因之一是其内置了丰富的、易用性良好 的数据结构。这些数据结构是实现复杂功能的利器,将它们与 Python 编程 基础知识灵活相结合就可以完成一些具有实用价值的编程任务了。 本章的大多数数据结构都在上一章中简单介绍过,这里对它们的使用 方法和特点做更加深入的介绍。首先,介绍序列数据类型共有的特点和使 用方法;然后,介绍几种常用的序列数据类型,包括列表、元组字符串等; 接下来介绍集合、字典;最后是 Python 标准库中其他常用的数据结构及使 用方法。 3.1 序 列 Python 中有多种序列数据类型,序列也是 Python 最为常用的数据结 构。本节介绍序列类型的种类以及所有序列类型通用的基本操作方法。 3.1.1 序列的种类 根据不同的划分角度,Python 中的序列类型有不同的划分方法。本小 节从序列元素的存储与组织方式、序列元素值是否可变两个角度对序列类 型进行划分。 1. 容器序列与扁平序列 根据序列元素的存储与组织方式,可将 Python 中的序列类型划分为 容器序列和扁平序列 2 种类型: . 容器序列(container sequences) . 容器序列中存储的并不是对象本身,而是对象的引用。因此,容器序 列对元素的数据类型没有任何要求。 . 常见容器序列类型有列表(list)、元组(tuple)、队列(collections.deque) 等,其中列表和元组较为常用。 . 扁平序列(flat sequences) . 扁平序列中存储的是对象的取值(即对象自身),需要一段连续的内存空间。因 此,扁平序列中的元素必须具有相同的数据类型。 . 常见扁平序列类型有字符串(str)、字节串(bytes)、字节数组(bytearray)、数 组(array.array)、内存视图(memoryview)等。其中字符串、字节串和数组较 为常用。 2. 可变序列与不可变序列 根据序列中存储的元素是否可以改变,可以将 Python 中的序列分为可变序列和不可 变序列 2 种类型: . 可变序列 . 序列中存储的元素可以被添加、删除、修改。 . 常见的可变序列类型有 list、bytearray、array.array、collections.deque、memoryview 等。 . 不可变序列 . 序列创建之后,其中的元素不能再添加、删除、修改。 . 常见的不可变序列类型有 tuple、str、bytes 等。 3.1.2 序列的基本操作 本小节介绍所有序列数据类型通用的常见操作方法,包括访问序列元素的索引与切片 操作、构造序列的连接与重复操作、判断元素与序列的隶属关系的操作,以及序列元素的 排序。 1. 索引与切片 序列是有序数据类型,序列中元素的索引就是其在序列中所在位置的序号。与 C/C++ 中的数组相似,使用索引每次可以访问序列中的一个元素。不同的是,Python 序列的索引 取值可以是 0、正数或负数。序列中第一个元素的索引为 0,最后一个元素索引为.1。正 值索引表示元素按从前向后顺序的位置序号,负值索引表示元素按从后向前顺序的位置序 号。图 3-1 所示的是一个序列 lst=[1,4,2,8,5,7] 及其索引。其中,中间为序列的元素,上部为 对应位置元素的正值索引,下部为相应的负值索引。该例中,lst[3] 与 lst[-3] 表示同一个元素。 图 3-1 序列的索引 >>> lst = [1, 4, 2, 8, 5, 7] 38 >>> lst[0] 1 >>> lst[-1] 7 >>> lst[3] == lst[-3] True 使用切片(slicing)能够一次访问序列中的多个元素。切片形如 i:j:k,表示索引值大于 等于 i 小于 j 步长为 k 的索引片段。若 i 为 0 则可以省去 i;若 j 等于序列长度则可以省 去 j;若步长 k 为 1,也可以省去 k,表示为 i:j 的形式。 >>> lst = [1, 4, 2, 8, 5, 7] >>> lst[1:5] [4, 2, 8, 5] >>> lst[:-1] [1, 4, 2, 8, 5] >>> lst[-5:-2] [4, 2, 8] >>> lst[:] [1, 4, 2, 8, 5, 7] >>> lst[::2] [1, 2, 5] 2. 连接与重复 连接和重复操作用于快速构造序列。 连接操作是两个序列相加,使用“+”运算符,得到的新序列包含了两个序列中的元 素。连接操作通常要求两个序列的类型相同。 >>> [1, 4, 2] + [8, 5, 7] [1, 4, 2, 8, 5, 7] >>> (1, 4, 2) + (8, 5, 7) (1, 4, 2, 8, 5, 7) >>> [1, 4, 2] + (8, 5, 7) # 序 列 和 元 组 类 型 不 同 不 能 连 接 Traceback (most recent call last): File "", line 1, in TypeError: can only concatenate list (not "tuple") to list 重复操作是将序列元素重复多次得到新的序列,使用“*”运算符。 >>> [1, 4, 2] * 3 [1, 4, 2, 1, 4, 2, 1, 4, 2] >>> 3 * 'Python ' 'Python Python Python ' 3. 元素检查 元素检查的目的是判断一个数值或对象是否包含在序列中,可使用 in 或 not in 运算 符,或者序列对象的 index 方法或 count 方法实现。 39 序列中包含目标元素时 in 运算符返回 True,否则返回 False,not in 运算符与之相反。 >>> lst = ['Python ', 'Java', 'C++'] >>> 'Python ' in lst True >>> 'C#' not in lst True 序列的 index 方法返回数值或对象在序列中的索引,若序列中不存在该值或对象则返 回错误。 >>> lst = ['Python ', 'Java', 'C++'] >>> lst.index('Python ') 0 >>> lst.index('C#') Traceback (most recent call last): File "", line 1, in ValueError: 'C#' is not in list 序列的 count 方法统计数值或对象在序列中的出现次数,若序列中不存在该值或对象 则返回 0。 >>> s = 'Hello Python ' >>> s.count('o') 2 4. 序列的其他操作 序列类型常用的其他操作还有: . len:返回序列的长度。 . max:返回序列中的最大值。 . min:返回序列中的最小值。 . sum:对序列元素值求和。 . sorted:对序列元素进行排序,不可变序列排序返回一个排序后的列表。 >>> lst = [1, 4, 2, 8, 5, 7] >>> len(lst) 6 >>> max(lst) 8 >>> min(lst) 1 >>> sum(lst) 27 >>> sorted(lst) [1, 2, 4, 5, 7, 8] >>> sorted(lst , reverse=True) # 逆 序 排 序 [8, 7, 5, 4, 2, 1] 40 3.2 列 表 本节详细介绍列表的定义与使用。列表可能是 Python 中最为重要的数据结构,也可 能是使用频率最高的数据结构。在 Python 中,列表的地位与其他语言中的数组相似,不 过使用更加灵活方便,可以将任何数据类型放进一个列表之中。 3.2.1 列表的定义 列表的类型是 list,可以使用方括号“[]”定义,也可以使用 list() 定义一个空列表。此 外,list 还可以作为类型转换函数用于将其他序列类型转换为列表。 >>> lst = [1, 4, 2, 8, 5, 7] >>> lst = list() >>> lst [] >>> s = 'Python ' >>> list(s) ['P', 'y', 't', 'h', 'o', 'n'] 3.2.2 列表元素的操作 列表是可变数据类型,因此除了序列类型通用的操作方法之外(见 3.1.2 小节),还可 以对列表元素进行添加、修改、删除等多种更新操作。 1. 添加元素 在一个列表的尾部添加元素使用 append(obj) 方法,其中 obj 为待添加元素。 >>> lst = [1, 4, 2, 8] >>> lst.append(5) >>> lst [1, 4, 2, 8, 5] 2. 插入元素 在列表中插入元素使用 insert(index, obj) 方法。index 为插入位置的索引,obj 为待插 入元素。 >>> lst = [1, 4, 2, 5] >>> lst.insert(4, 8) >>> lst [1, 4, 2, 5, 8] 3. 列表扩充 列表的扩充是指在列表尾部一次性添加多个元素,使用 extend(seq_obj) 方法实现。其中, seq_obj 为待添加元素构成的序列。这种方法的效果与序列连接相同,但运行效率更高。 41 >>> lst = [1, 4, 2, 8] >>> lst.extend ([5, 7]) >>> lst [1, 4, 2, 8, 5, 7] 4. 删除元素 删除列表元素有多种途径。可以使用 del 函数或者 list 类型的 remove(obj) 方法,也 可以使用 clear 方法清空列表。 >>> lst = [1, 4, 2, 8, 5, 7] >>> del lst[0] >>> lst [4, 2, 8, 5, 7] >>> lst.remove(7) >>> lst [4, 2, 8, 5] >>> lst.clear () >>> lst [] 5. 修改元素值 修改列表元素的值可以使用索引也可以使用切片的方法。使用索引一次可以修改一个 元素,使用切片一次可修改多个元素。切片的方法能够实现比较复杂的效果,使用非常灵 活。在步长缺省的情况下,能够实现插入多个元素的效果。不过,使用切片的方法会降低 代码的可读性,建议谨慎使用。 >>> lst = [1, 4, 2, 8] >>> lst[-1] = 7 # 利 用 索 引 修 改 元 素 >>> lst [1, 4, 2, 7] >>> lst[0:2] = [5, 7] # 修 改 前 两 个 元 素 >>> lst [5, 7, 2, 7] >>> lst[::2] = [0, 0] # 修 改 索 引 为 偶 数 的 元 素 >>> lst [0, 7, 0, 7] >>> lst[2:2] = [4, 2, 4, 2] # 插 入 多 个 元 素 >>> lst [0, 7, 4, 2, 4, 2, 0, 7] 6. 排序 在 3.1.2 小节中介绍了用于序列排序的 sorted 函数。列表的排序除了可以使用 sorted 函数之外,还可以使用 list 类型的 sort 方法。这 2 种方法效果相同,但是其实现原理完全 不同。sorted 函数会复制出一份新的列表在新的列表上排序,原来的列表不会被改变。而 sort 方法则在原列表上对元素重新排序。 42 函数 id 用于获取对象的内存地址,可作为 Python 对象的唯一身份标识。两个变量的 id 值相同,表示它们是同一个对象。下面的例子中利用 id 函数分析 sorted 与 list.sort 的 区别。 >>> lst = [1, 4, 2, 8, 5, 7] >>> lst_id = id(lst) >>> lst_sorted = sorted(lst) >>> lst_id == id(lst_sorted) False >>> lst.sort() >>> lst_id == id(lst) True 列表类型的 reverse 方法用于反转列表元素的顺序。该方法也是在序列上操作,不会返 回新的序列。 >>> lst = [1, 4, 2, 8, 5, 7] >>> lst_id = id(lst) >>> lst.reverse () >>> lst [7, 5, 8, 2, 4, 1] >>> lst_id == id(lst) True 7. 复制 list 类型的 copy 方法用于复制列表,返回一个与原列表完全相同的新列表。有时候我 们需要改变列表的值,但是在后续代码中还会用到原来的序列。这种情况下就需要将序列 复制一份。 >>> lst = [1, 4, 2, 8, 5, 7] >>> lst_new = lst.copy() >>> lst_new.sort() >>> lst_new [1, 2, 4, 5, 7, 8] >>> lst [1, 4, 2, 8, 5, 7] 不过,list 是容器类序列,list.copy 方法复制的仅仅是列表中存储的引用值。下面的例 子中有两个列表,lst 以及利用 copy 方法复制出新的列表 lst_new。在修改 lst_new 的最 后一个元素 lst_new[-1] 之后,发现 lst[-1] 的值发生相同的变化,说明 lst_new[-1] 与 lst[-1] 指向同一个对象。 >>> lst = [1, 4, 2, 8, [5, 7]] >>> lst_new = lst.copy() >>> lst_new[-1][0] = 0 >>> lst_new [1, 4, 2, 8, [0, 7]] 43 >>> lst [1, 4, 2, 8, [0, 7]] 使用 list 类型的 copy 方法进行复制称为浅复制。利用 copy 模块中的函数 copy 可以 实现相同的效果。将列表以及列表中的元素乃至元素中保存的子元素全部复制一份,这种 方法称为深复制。使用 copy 模块中的 deepcopy 函数可实现深复制。 >>> from copy import deepcopy >>> lst = [1, 4, 2, 8, [5, 7]] >>> lst_new = deepcopy(lst) >>> lst_new[-1][0] = 0 >>> lst_new [1, 4, 2, 8, [0, 7]] >>> lst [1, 4, 2, 8, [5, 7]] 注意,copy 模块中的 copy 函数和 deepcopy 函数可以复制任意 Python 数据类型,而 不仅仅是列表。 3.2.3 列表推导式 Python 列表推导式的作用是基于一个可迭代对象à创建一个新的列表。当需要对一个 序列或者其他可迭代对象进行遍历时,使用列表推导式非常方便、直观,而且执行效率高。 作为一种动态语言,Python 的运行效率比编译运行的静态语言低很多。循环语句对运行效 率的影响尤其明显,因此在 Python 中应尽可能减少循环语句的使用。列表推导式就是循 环语句的一种很好的替代。 列表推导式的语法形式为: [ 表 达 式 for ...in iter_obj] 下例中,利用列表推导式对一个由字符组成的列表中的元素进行类型转换,创建一个 新的由整数组成的列表: >>> lst_str = ['1', '4', '2', '8', '5', '7'] >>> lst = [int(s) for s in lst_str] >>> lst [1, 4, 2, 8, 5, 7] 列表推导式可以同时遍历多个可迭代对象创建新的列表。语法形式为: [ 表 达 式 for ...in iter_obj1 for ... in iter_obj2 ...] 下面的例子中,将两个序列中每一对元素相加得到一个新的序列,相当于在两个序列 上进行两重循环。 >>> lst1 = [11 , 12 , 13] >>> lst2 = [21 , 22 , 23] à 参见第 6.1.8 小节。 44 >>> lst_new = [e1 + e2 for e1 in lst1 for e2 in lst2] >>> lst_new [32 , 33 , 34 , 33 , 34 , 35 , 34 , 35 , 36] >>> lst_new = [(e1 , e2 , e1 + e2) for e1 in lst1 for e2 in lst2] >>> lst_new [(11 , 21 , 32), (11 , 22 , 33), (11 , 23 , 34), (12 , 21 , 33), (12 , 22 , 34), (12 , 23 , 35), (13 , 21 , 34), (13 , 22 , 35), (13 , 23 , 36)] 列表推导式还有一个 if 子句,用于根据逻辑表达式对新生成的列表元素进行过滤。完 整的列表推导式的语法形式为: [ 表 达 式 for ...in list_obj1 for ... in list_obj2 ... if 逻 辑 表 达 式 ] 下面的例子利用列表推导式的 if 子句实现了两个列表对应位置元素相加的效果: >>> lst1 = [11 , 12 , 13] >>> lst2 = [21 , 22 , 23] >>> lst_add = [e1 + e2 for i, e1 in enumerate(lst1) for j, e2 in enumerate(lst2) if i == j] >>> lst_add [32 , 34 , 36] 列表推导式的运行速度要比循环快。下面的例子分别使用循环和列表推导式构造一个 长度为 1 亿个元素的整数列表,并显示其运行时间。 1 from time import time 2 start_time = time() 3 l = [] 4 for i in range(100000000): 5 l.append(i) 6 print(time()-start_time) 1 from time import time 2 start_time = time() 3 l = [i for i in range(100000000)] 4 print(time()-start_time) 运行时间分别为 14.29 秒和 5.53 秒à,列表推导式明显比循环速度快。不过要得到同 样的列表还有更快、更便捷的方式: 1 from time import time 2 start_time = time() 3 l = list(range(100000000)) 4 print(time()-start_time) 大约只需要 3 秒! à 具体运行时间取决于计算机软硬件环境。 45 从上述 3 种方式可看出,在 Python 中实现同样的功能往往会有多种不同的方法,但 它们的运行效率可能差别非常大。所以,在学习 Python 时一定要尽可能去理解代码背后 的工作原理。 3.2.4 栈 栈的特征是后进先出(last in first out)。Python 列表中添加或移除最后一个元素非常 高效,因而适合作为栈来使用。list 类型的 pop 方法作用是弹出列表的最后一个元素,即 出栈操作。Python 列表没有 push 方法,但是 append 方法的作用与入栈的 push 操作完 全相同。 >>> stack = [1, 4, 2, 8, 5, 7] >>> stack.pop() 7 >>> stack.append(7) >>> stack [1, 4, 2, 8, 5, 7] 也可以利用 Python 动态语言的特性,使得栈的操作更加符合使用习惯。 >>> stack = [1, 4, 2, 8, 5, 7] >>> pop = stack.pop >>> push = stack.append >>> pop() 7 >>> stack [1, 4, 2, 8, 5] >>> push(7) >>> stack [1, 4, 2, 8, 5, 7] 3.3 元 组 元组也是一种非常重要的数据结构,它与列表在定义和使用上都非常相似,只不过它 是一种不可变数据类型。 3.3.1 定义和使用 元组使用圆括号“()”定义,它的类型为 tuple。可以使用 tuple 将一个序列转换为一 个元组。tuple() 也可以用于定义一个空元组,但是元组是不可变序列,空元组实际上没有 什么意义。 >>> lst = [1, 4, 2, 8, 5, 7] >>> t = tuple(lst) >>> t (1, 4, 2, 8, 5, 7) 46 >>> s = 'Python ' >>> t = tuple(s) >>> t ('P', 'y', 't', 'h', 'o', 'n') 需要注意的是,当元组中只有一个元素时,使用 () 定义必须要在元素后边加一个逗号。 如下例所示,如果没有逗号,得到的并不是元组。 >>> t = (1) >>> t 1 >>> type(t) >>> t = (1,) >>> type(t) 元组的操作主要包括序列的基本操作,索引、切片、连接、重复、元素检查,以及计 算长度、最大值、最小值等。除元素不可改变之外,元组与列表的操作完全相同。 3.3.2 元组的不可变陷阱 元组属于不可变数据类型,从表面上看意味着其元素是不可改变的。关于这点,元组 有一个著名的陷阱,如不留意很容易留下难以发现的错误。 如果将一个可变序列作为元组的元素,那么这个可变序列的元素是否能够被改变呢? 答案是肯定的。原因在于元组同时还是一个容器类型,其中存储的并不是元素自身而是元 素的引用。“不可变”是指其中存储的引用是不能改变的,并不意味着引用指向的对象是不 可改变的。 下面的例子中,t 是一个元组,索引为 2 的元素为一个列表。虽然无法改变 t[2] 的引 用值,但是引用指向的列表的值是可变对象,其值是可以改变的。 >>> t = (1, 'w', [4, 2]) >>> t[2] = 1 Traceback (most recent call last): File "", line 1, in TypeError: 'tuple ' object does not support item assignment >>> t[2][0]=0 >>> t (1, 'w', [0, 2]) 这说明 tuple 并非是绝对不可改变的数据类型,在实际使用中要特别注意。 3.3.3 生成器推导式. 本质上来说,生成器推导式与元组其实关系不大。放在这里介绍的原因是生成器推导 式的定义使用了“()”,这使得元组和生成器推导式的关系从语法形式上来说与列表和列表 生成式的关系相似。 47 生成器推导式的语法形式与列表推导式的唯一区别就是将“[]”换为“()”: ( 表 达 式 for ...in iter_obj) 与列表推导式相同,生成器推导式中也可以包含多个 for 子句以及 if 子句。生成器推 导式与列表推导式最大的区别是,它返回的是一个生成器。生成器à是一种特殊的可迭代对 象,它并没有保存所有的元素,而是仅仅定义了获取下一个元素的方法。而“下一个”元 素是什么,要等被迭代时再经过计算得到。生成器实际上是一种延时计算的手段。 下面的例子对比了创建包含 1 亿个元素的列表和生成器所需的时间和内存空间。 1 from time import time 2 from sys import getsizeof 3 4 start_time = time() 5 lst = [i for i in range(100000000)] 6 print(time()-start_time) 7 print(getsizeof(lst)) 8 9 start_time = time() 10 g = (i for i in range(100000000)) 11 print(time()-start_time) 12 print(getsizeof(g)) 运行结果显示,创建列表需要大约 5 秒和 800 MB 的空间;而创建生成器需要 7×10.6 秒和 112 B 的空间,时间和空间都几乎可以忽略。 关于生成器更详细的内容将在第 6 章介绍。 3.4 集 合 集合是有别于列表和元组的一种重要的容器类型,但集合不是序列。集合中不允许存 在重复元素,并且元素是无序的,这与数学中集合的概念完全相同。本节介绍集合的定义 和使用方法。 3.4.1 集合的定义 Python 中的集合类型有可变集合和不可变集合两种,分别为 set 和 frozenset。使 用 set() 可以定义一个空的可变集合,也可以将其他可迭代对象转为一个可变集合类型。 frozenset() 可定义一个空的不可变集合,或者将其他可迭代对象转为一个不可变集合。这 2 种集合类型的特点和使用方法类似,区别仅在于 frozenset 的元素是不可改变的。二者相 比,set 的应用较多而 frozenset 的应用则相对较少,因此下文中仅详细介绍可变集合 set。 集合中不允许出现重复元素,它会自动将重复元素去除。非空集合可以用“{}”定义。 >>> s = {1, 4, 2, 1, 2, 7} >>> s à 参见第 6.2 节。 48 {1, 2, 4, 7} 利用集合的这种特点,可以非常高效地去除序列中的重复元素。 >>> lst = list(range(10000)) * 2 >>> len(lst) 20000 >>> s = set(lst) >>> len(s) 10000 >>> 3.4.2 常用集合操作方法 1. 集合元素的操作 集合中的元素是无序的,不能像序列那样使用索引或者切片的方式访问单个或部分元 素。不过,集合是一种可迭代对象,可以使用循环语句或推导式来遍历每个元素。对于可 变集合,可以添加或去除元素。由于没有索引可用,在去除元素时只能以元素自身作为参 数,或者随机去除元素。 常见的集合元素操作如表 3-1 所示。操作集合元素时,除了集合对象自身的这些方法 之外,用于求元素数量(len)、最大值(max)、最小值(min)、求和(sum)等的函数对 集合对象也有效。 表 3-1 集合元素的操作 方 法 功 能 示 例 结 果 add 添加一个元素 {1,4,2}.add(8) {8,1,2,4} set.update 添加多个元素 {1,4}.update({2,8}) {8,1,2,4} remove 去除元素 {1,4,2,8}.remove(0) KeyError set.discard 去除元素 {1,4,2,8}.discard(0) {8,1,2,4} pop 随机弹出元素 {1,4,2,8}.pop() 8 clear 清空集合 {1,4,2,8}.clear() {} 2. 集合运算 Python 中的集合类型具有和数学中集合相同的交、并、差以及对称差等集合运算。集 合运算既可以使用运算符也可以使用集合对象的方法实现,见表 3-2。 表 3-2 集合运算 运 算 符 方 法 运 算 示 例 结 果 & intersection 交 {1,2,3} & {4,2} {2} | union 并 {1,2,3} | {4,2} {1,2,3,4} - difference 差 {1,2,3} - {4,2} {1,3} ^ symmetric_difference 对称差 {1,2,3} ^ {4,2} {1,3,4} 49 3. 集合关系检查 判断集合中是否包含一个元素的方法与序列相同,使用 in 或 not in 运算符。但 in 或 not in 不能用于比较两个集合之间的关系。集合类型提供了相应的方法来判断两个集合之 间的关系,见表 3-3。 表 3-3 集合关系检查 方 法 功 能 示 例 结 果 isdisjoint 交集是否为空 {1,2,3}.isdisjoint({1,2}) False issubset 是否为子集 {1,2}.issubset({1,2,3}) True issuperset 是否为超集 {1,2,3}.issuperset({1,2}) True 两个集合关系判断更方便的方法是使用比较运算符:集合相等(==)、集合不全相等 (!=)、子集(<=)、真子集(<)、超集(>=)、真超集(>)。 3.4.3 集合推导式 集合推导式与列表推导式和生成器推导式的概念相似,可以不使用循环而基于一个可 迭代对象创建一个集合。集合推导式的语法形式与列表推导式的唯一区别就是将 [] 换为 {}。语法形式为: { 表 达 式 for ...in iter_obj} 当然,集合推导式中也可以包含多个 for 子句以及 if 子句。下例中,给出一组学生的 多门课程的成绩记录,计算出一共有多少位同学曾经参加了考试。 >>> scores = [ ... (' 张 三 ', ' 数 学 ', 90), ... (' 李 四 ', ' 数 学 ', 80), ... (' 王 五 ', ' 英 语 ', 85), ... (' 张 三 ', ' 英 语 ', 95) ... ] >>> name_set = {e[0] for e in scores} # 集 合 推 导 式 >>> print(' 有 ', len(name_set), ' 人 参 加 了 考 试 ') 有 3 人 参 加 了 考 试 3.4.4 排列组合. 排列组合是组合数学中的基本概念。所谓排列,是从 n 个元素中不重复地取出 m(m . n) 个元素并按一定的顺序排列。排列是有序的,所有可能的排列的个数,称为排列数,记为: Amn = n(n . 1)(n . 2) · · · (n . m + 1) = n! (n . m)! 所谓组合,是从 n 个元素中不重复地取出 m(m . n) 个元素,称为一个组合,组合是 无序的。所有可能的组合的总数称为组合数,记为: 50 Cm n = Amn Am = n! m!(n . m)! 在实际应用中,通常不仅需要计算排列数和组合数,还要给出所有排列或组合。排列 或组合算法的计算复杂度比较高,手动实现代码虽然不复杂,但是运行效率会很低。 Python 的 itertools 模块提供了两个函数 permutations 和 combinations 分别用于排 列和组合的运算。需要注意的是,permutations 和 combinations 不只可用于集合,还可以 用于列表和元组。这种情况下,会将列表中的每个元素作为独立的集合元素,不管其取值 是否相同。 >>> from itertools import permutations , combinations >>> s = {1, 4, 2, 8} >>> p = permutations(s, 2) # 排 列 >>> list(p) [(8, 1), (8, 2), (8, 4), (1, 8), (1, 2), (1, 4), (2, 8), (2, 1), (2, 4), (4, 8), (4, 1), (4, 2)] >>> c = combinations(s, 2) # 组 合 >>> list(c) [(8, 1), (8, 2), (8, 4), (1, 2), (1, 4), (2, 4)] 3.5 字 典 字典也是一种使用频率非常高的数据结构,它既不同于序列也不同于集合,是以键-值 对(key-value)的形式存储元素的一种容器。 3.5.1 字典的定义 字典的定义也使用“{}”,形如 {key:value,...}。字典的类型为 dict,dict() 可以定义一 个空字典(也可以使用“{}”定义空字典),也可以将一个由元组组成的列表转换为字典。 >>> d = dict(a=1, b=4, c=2, d=8) >>> d {'a': 1, 'b': 4, 'c': 2, 'd': 8} >>> lst = [('a', 1), ('b', 4), ('c', 2), ('d', 8)] >>> dict(lst) {'a': 1, 'b': 4, 'c': 2, 'd': 8} 字典元素的 value 可以是任意对象,但是 key 必须是可哈希(hashable)对象。哈希函 数能够为每个不同的输入计算一个唯一的输出,称为哈希值。Python 中对象的可哈希性, 就是哈希值与对象取值之间有一一对应的映射关系。对于可变对象来说,无法建立起哈希 值与取值间的对应关系(或者说无法计算哈希值),因此都是不可哈希对象。不可变对象的 值不能改变,但可以计算哈希值,因此都是可哈希对象。所以,也可以说字典的键必须是 不可变对象。 通常,字典的键要使用简单的数据类型,最常使用的是字符串。 51 3.5.2 字典常用操作方法 1. 字典元素的操作 字典元素的访问、修改、删除使用 key 实现。 >>> d = {'a':1, 'b':4, 'c':2, 'd':8} >>> d['a'] 1 >>> d['e'] = 5 >>> d {'a': 1, 'b': 4, 'c': 2, 'd': 8, 'e': 5} >>> del d['e'] >>> d {'a': 1, 'b': 4, 'c': 2, 'd': 8} 在使用 key 访问元素时,若 key 不存在会抛出 KeyError。更安全的方法是使用字典 对象的 get 方法,key 不存在时返回 None 或者指定的默认值。 >>> d = {'a':1, 'b':4, 'c':2, 'd':8} >>> d['e'] Traceback (most recent call last): File "", line 1, in KeyError: 'e' >>> d.get('e') is None True >>> d.get('e', 'no exist ') 'no exist ' 由于使用 key 来访问元素,所以字典中元素的顺序并不重要。早期 Python 版本中,字 典中元素就是无序的。Python 3.6 中使用了新的算法,字典中元素是按添加的先后顺序排 序的。于是,字典实际上成为一种有序数据结构,在 Python 3.8 中可以使用 reversed 函数 反转字典元素的顺序。 2. 字典的遍历 字典的遍历有多种方法,可以只遍历所有的 key 或 value,也可以遍历所有的 key-value 对。分别通过字典对象的如下 3 种方法实现: . keys:返回字典对象中所有 key 组成的可迭代对象。 . values:返回由所有 value 组成的可迭代对象。 . items:返回由元组 (key, value) 组成的可迭代对象。 >>> d = {'a':1, 'b':4, 'c':2, 'd':8} >>> d.keys() dict_keys (['a', 'b', 'c', 'd']) >>> d.values () dict_values ([1, 4, 2, 8]) >>> d.items () dict_items ([('a', 1), ('b', 4), ('c', 2), ('d', 8)]) 52 遍历一个字典的所有 key-value 对有如下 2 种方法。也可以利用推导式遍历字典以避 免循环的使用。 1 d = {'a': 1, 'b': 4, 'c':2, 'd': 8} 2 for key in d.keys(): # 方法 1 3 print(key , d[key]) 4 5 for key , value in d.items (): # 方法 2 6 print(key , value) 字典也可以使用 in 或 not in 运算符进行元素检查。不过只能用于判断字典中是否包 含了指定的 key,key in dict_obj 相当于 key in dict_obj.keys()。 3. 字典的其他操作方法 字典的其他常用操作方法见表 3-4。 表 3-4 字典的其他常用操作方法 方 法 功 能 示 例 结 果 clear 清空字典 {'a':1, 'b':4, 'c':2, 'd':8}.clear() {} popitem 弹出一个键-值对 {'a':1, 'b':4, 'c':2, 'd':8}.popitem() ('d', 8) pop 弹出指定的值 {'a':1, 'b':4, 'c':2, 'd':8}.pop('a') 1 update 利用另一个字典 {'a':4, 'b':2}.update({'a':1, 'b':3}) {'a': 1, 'b': 3} 更新当前字典 3.5.3 字典推导式 字典推导式与列表推导式和集合推导式类似,在不使用循环的情况下创建字典。语法 形式为: {key:value for key , value in iter_obj} 字典推导式中也可以包含多个 for 子句以及 if 子句,用于构造更复杂的字典。下面例 子中,基于一个字符串列表构建一个键为字符串、值为字符串长度的字典: >>> languages = ['Python ', 'Java', 'C', 'C++', 'C#'] >>> language_len = {e: len(e) for e in languages} >>> language_len {'Python ': 6, 'Java': 4, 'C': 1, 'C++': 3, 'C#': 2} 3.6 字 符 串 字符串在所有编程语言中都很重要,它在 Python 中有着独特的地位。Python 语言最 初引起数据分析领域的重视就是因为它在字符串处理方面功能强大且简单易用。本节详细 介绍 Python 的字符串类型及字符串处理方法。 53 3.6.1 字符串的定义 字符串的定义在第 2.3.1 节中已经介绍过,定义字符串可使用一对单引号“'”、双引 号“"”或者 2 种三引号“'''”和“"""”。字符串的类型为 str,可以使用 str() 定义一个 空字符串,或者将任意数据类型转换为字符串。 定义字符串还可以使用不同的前缀,u 前缀可定义 Unicode 字符串;使用 r 前缀可创 建原始字符串;使用 f 前缀可创建格式字符串。 3.6.2 常用字符串处理方法 Python 中的字符串处理方法非常丰富,本小节根据功能将其划分为 7 个类别。字符串 的处理通常都比较简单、直观,因此本小节仅以表格形式给出操作方法的名称、功能,以 及最常见的应用示例。 1. 大小写转换 字符串的大小写转换包括将所有字母转为大写或小写、字母大小写互相转换、单词首 字母转换等。常用的方法和示例如表 3-5 所示。 表 3-5 字符串大小写转换 方 法 功 能 示 例 方法调用 输 出 lower 所有字母转为小写 s='Python' s.lower() 'python' upper 所有字母转为大写 s='Python' s.upper() 'PYTHON' swapcase 大小写互换 s='Python' s.swapcase() 'pYTHON' title 单词首字母大写 s='python is easy' s.title() 'Python Is Easy' 2. 空白去除与填充 字符串空白去除的目的是清除掉两端的空白符号,包括空格、Tab 符号等。这种操作 在数据处理中常常用到,例如读取以文本形式存储的数据、用户输入数据的处理等。字符 填充与空白去除相反,利用指定符号填充字符串的两端,常用于格式化输出。常用的方法 和应用示例如表 3-6 所示,其中“方法调用”列中使用的参数“10”表示输出字符串的总 长度。 表 3-6 字符串空白去除与填充 方 法 功 能 示 例 方法调用 输 出 strip 去除两侧空白 s=' Python ' s.strip() 'Python' lstrip 去除左侧空白 s=' Python ' s.lstrip() 'Python ' rstrip 去除右侧空白 s=' Python ' s.rstrip() ' Python' center 两侧填充符号 s='Python' s.center(10, '*') '**Python**' ljust 右侧填充符号 s='Python' s.ljust(10, '*') 'Python****' rjust 左侧填充符号 s='Python' s.rjust(10, '*') '****Python' zfill 左侧补 0 s='Python' s.zfill(10) '0000Python' 3. 查找、替换与翻译 字符串的查找和替换常见的方法和应用如表 3-7 所示。 54 表 3-7 字符串查找与替换 方 法 功 能 示 例 方法调用 输 出 find 从左开始查找子串, s='Hello Python' s.find('o') 4 返回索引或-1 rfind 从右开始查找子串, s='Hello Python' s.rfind('o') 10 返回索引或-1 index 从左开始查找子串, s='Hello Python' s.index('o') 4 返回索引或错误 rindex 从右开始查找子串, s='Hello Python' s.rindex('o') 10 返回索引或错误 replace 替换子串 s='Hello Python' s.replace('P', 'J') 'Hello Jython' expandtabs Tab 替换为空格 s='Hello\tPython' s.expandtabs(2) 'Hello Python' 注:“\t”为 Tab 符号。 字符串翻译的功能是令多对符号互换,需用到 translate 方法和字符串类型的类方法à str.maketrans。下面的例子中,将符号 abc 和 123 成对互换。 >>> trans=str.maketrans('abc', '123') >>> s = 'abccba ' >>> s.translate(trans) '123321 ' 4. 分割 字符串分割是指利用特殊的符号将字符串切分为多个部分,返回以切分后的多个子串 为元素的列表或元组。常见方法和应用如表 3-8 所示。 表 3-8 字符串分割 方 法 功 能 示 例 方法调用 输 出 split 从左侧开始分割 s='a,b,c' s.split(',') ['a', 'b', 'c'] rsplit 从右侧开始分割 s='a,b,c' s.rsplit(',', 1) ['a,b', 'c'] splitlines 按行分割 s='a\nb\nc' s.splitlines() ['a', 'b', 'c'] partition 从左侧开始切断 s='a,b,c' s.partition(',') ('a', ',', 'b,c') rpartition 从右侧开始切断 s='a,b,c' s.rpartition(',') ('a,b', ',', 'c') 注:“\n”为换行符。 5. 连接 字符串连接是指使用 join 方法将可迭代对象中的元素利用给定字符串连接起来。如下 例所示: >>> lst = ['1', '4', '2', '8'] >>> '-'.join(lst) '1-4-2-8' à 类方法可通过类名调用,参见第 5.4 节。 55 6. 特征测试 字符串的特征测试用于判断字符串是否具有某种特点,返回 bool 值。常用特征测试方 法如表 3-9 所示。 表 3-9 字符串特征测试 方 法 功 能 示 例 方法调用 输 出 isalnum 是否仅含字母或数字 s='Abc123' s.isalnum() True isdecimal 是否仅含十进制数字 s='123' s.isdecimal() True isalpha 是否仅含字母 s='Abc123' s.isalpha() False isdigit 是否仅含整数数字 s='123.0' s.isdigit() False isnumeric 是否仅含数字 s='123' s.isnumeric() True isupper 是否不含小写符号 s='ABC123' s.isupper() True islower 是否不含大写符号 s='abc123' s.islower() True isspace 是否仅含空白符号 s=' \t' s.isspace() True istitle 是否首字母大写 s='Python is easy' s.istitle() False isascii 是否仅含 ASCII 符号 s='Python 编程' s.isascii() False isprintable 是否为可打印符号 s='\t' s.isprintable() False startswith 是否以给定子串开头 s='Abc123' s.startswith('Abc') True endswith 是否以给定子串结尾 s='Abc123' s.endswith('123') True 7. 动态执行 * Python 能够动态地执行代码,这也是动态语言带来的优势。这里所谓动态执行代码, 就是在 Python 程序中运行包含了合法 Python 代码的字符串。 Python 提供了两个函数 exec 和 eval 来动态地执行代码。二者的区别在于,exec 用于 将包含了代码的字符串作为脚本代码执行,无返回值;而 eval 则用于执行一个包含了表达 式的字符串,返回表达式的执行结果。 >>> exec('print (" Hello Python !")') Hello Python! >>> eval('5 > 3') True >>> lst = eval('[1, 4, 8, 2, 7]') >>> lst [1, 4, 8, 2, 7] 动态执行代码有时候会带来很大的方便,实现一些比较灵活的功能。但是也可能会带 来很大的风险,特别是在被执行的代码中包含了用户输入内容时,很容易出现代码注入攻 击漏洞。所以,动态执行代码必须要在有安全保障的情况下非常谨慎地使用。 3.6.3 字符串格式化 字符串格式化是将一个字符串模板中的占位符替换为所需输出的具体取值,从而实现 复杂的字符串输出。 Python 中有 3 种格式化字符串的方法。第一种方法以“%”表示的特殊字符串作为占 位符。这种方法与 C 语言中 printf 函数的使用方法相似,是 Python 最早支持的字符串格 56