流畅的 Python 第二版(GPT 重译)(一)(4)

简介: 流畅的 Python 第二版(GPT 重译)(一)

流畅的 Python 第二版(GPT 重译)(一)(3)https://developer.aliyun.com/article/1484366

A += 赋值谜题

尝试在不使用控制台的情况下回答:评估 示例 2-16 中的两个表达式的结果是什么?⁹

示例 2-16. 一个谜题
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]

接下来会发生什么?选择最佳答案:

  1. t 变成了 (1, 2, [30, 40, 50, 60])
  2. 引发 TypeError,消息为 'tuple' object does not support item assignment
  3. 无。
  4. A 和 B 都是。

当我看到这个时,我非常确定答案是 B,但实际上是 D,“A 和 B 都是”!示例 2-17 是来自 Python 3.9 控制台的实际输出。¹⁰

示例 2-17. 意外结果:项目 t2 被更改 并且 引发异常
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])

在线 Python Tutor 是一个很棒的在线工具,可以详细展示 Python 的工作原理。图 2-5 是两个截图的组合,显示了来自 示例 2-17 的元组 t 的初始状态和最终状态。

图 2-5. 元组赋值谜题的初始状态和最终状态(由在线 Python Tutor 生成的图表)。

当你查看 Python 为表达式 s[a] += b 生成的字节码时(示例 2-18),就会清楚这是如何发生的。

示例 2-18. 表达式 s[a] += b 的字节码
>>> dis.dis('s[a] += b')
 1           0 LOAD_NAME                0 (s) 3 LOAD_NAME                1 (a) 6 DUP_TOP_TWO 7 BINARY_SUBSCR # ①
 8 LOAD_NAME                2 (b) 11 INPLACE_ADD # ②
 12 ROT_THREE 13 STORE_SUBSCR # ③
 14 LOAD_CONST               0 (None) 17 RETURN_VALUE

s[a] 的值放在 TOS(栈顶)上。

执行 TOS += b。如果 TOS 指向一个可变对象(就像在 示例 2-17 中的列表),那么这将成功。

s[a] = TOS。如果 s 是不可变的(例如 示例 2-17 中的元组 t),则此操作失败。

这个例子是一个非常特殊的情况,在使用 Python 20 年中,我从未见过这种奇怪的行为实际上影响到任何人。

我从中得到了三个教训:

  • 避免将可变项放入元组中。
  • 增强赋值不是一个原子操作——我们刚刚看到它在完成部分工作后抛出异常。
  • 检查 Python 字节码并不太困难,而且可以帮助我们了解底层发生了什么。

在见识了使用+*进行连接的微妙之后,我们可以将话题转向另一个与序列相关的重要操作:排序。

list.sort与内置的sorted的比较

list.sort方法原地对列表进行排序,即不创建副本。它返回None以提醒我们它改变了接收者¹¹,并且没有创建新列表。这是一个重要的 Python API 约定:在原地更改对象的函数或方法应该返回None,以明确告诉调用者接收者已被更改,没有创建新对象。例如,random.shuffle(s)函数也表现出类似的行为,它原地对可变序列s进行洗牌,并返回None

注意

返回None以表示原地更改的约定存在一个缺点:我们无法级联调用这些方法。相反,返回新对象的方法(例如,所有str方法)可以以流畅接口风格级联。请参阅维基百科的“流畅接口”条目以进一步描述这个主题。

相反,内置函数sorted创建一个新列表并返回它。它接受任何可迭代对象作为参数,包括不可变序列和生成器(参见第十七章)。无论给sorted的可迭代对象的类型是什么,它总是返回一个新创建的列表。

list.sortsorted都接受两个可选的、仅限关键字的参数:

reverse

如果为True,则按降序(即,通过反转项目的比较)返回项目。默认值为False

key

一个参数函数,将被应用于每个项目以生成其排序键。例如,当对字符串列表进行排序时,可以使用key=str.lower执行不区分大小写的排序,key=len将按字符长度对字符串进行排序。默认是恒等函数(即,比较项目本身)。

提示

您还可以在min()max()内置函数以及标准库中的其他函数(例如itertools.groupby()heapq.nlargest())中使用可选的关键字参数key

这里有一些示例来澄清这些函数和关键字参数的使用。这些示例还演示了 Python 的排序算法是稳定的(即,它保留了相等比较的项目的相对顺序):¹²

>>> fruits = ['grape', 'raspberry', 'apple', 'banana']
>>> sorted(fruits)
['apple', 'banana', 'grape', 'raspberry'] # ①
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] # ②
>>> sorted(fruits, reverse=True)
['raspberry', 'grape', 'banana', 'apple'] # ③
>>> sorted(fruits, key=len)
['grape', 'apple', 'banana', 'raspberry'] # ④
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple'] # ⑤
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] # ⑥
>>> fruits.sort()                          # ⑦
>>> fruits
['apple', 'banana', 'grape', 'raspberry'] # ⑧

这将产生一个按字母顺序排序的新字符串列表。¹³

检查原始列表,我们看到它没有改变。

这是之前的“字母顺序”,但是反转了。

一个按长度排序的新字符串列表。由于排序算法是稳定的,“葡萄”和“苹果”,长度均为 5,按原始顺序排列。

这些是按长度降序排序的字符串。这不是前一个结果的反转,因为排序是稳定的,所以“葡萄”再次出现在“苹果”之前。

到目前为止,原始fruits列表的顺序没有改变。

这会原地对列表进行排序,并返回None(控制台省略了这一点)。

现在fruits已经排序。

警告

默认情况下,Python 按字符代码按字典顺序对字符串进行排序。这意味着 ASCII 大写字母将排在小写字母之前,非 ASCII 字符不太可能以合理的方式排序。“对 Unicode 文本进行排序”介绍了按人类期望的方式对文本进行排序的正确方法。

一旦您的序列被排序,它们可以被非常高效地搜索。Python 标准库的bisect模块中已经提供了二分搜索算法。该模块还包括bisect.insort函数,您可以使用它来确保您的排序序列保持排序。您可以在fluentpython.com伴随网站的“使用 Bisect 管理有序序列”文章中找到bisect模块的图解介绍。

到目前为止,在本章中所看到的大部分内容都适用于一般序列,而不仅仅是列表或元组。Python 程序员有时会过度使用list类型,因为它非常方便——我知道我曾经这样做过。例如,如果您正在处理大量数字列表,应考虑改用数组。本章的其余部分致力于列表和元组的替代方案。

当列表不是答案时

list类型灵活且易于使用,但根据具体要求,有更好的选择。例如,当需要处理数百万个浮点值时,array可以节省大量内存。另一方面,如果您不断地向列表的两端添加和删除项目,那么了解deque(双端队列)是一种更高效的 FIFO¹⁴数据结构是很有用的。

提示

如果您的代码经常检查集合中是否存在某个项目(例如,item in my_collection),请考虑使用set代替my_collection,特别是如果它包含大量项目。集合针对快速成员检查进行了优化。它们也是可迭代的,但它们不是序列,因为集合项的顺序是未指定的。我们将在第三章中介绍它们。

在本章的其余部分中,我们将讨论可以在许多情况下替代列表的可变序列类型,从数组开始。

数组

如果列表只包含数字,array.array是更高效的替代品。数组支持所有可变序列操作(包括.pop.insert.extend),以及用于快速加载和保存的附加方法,如.frombytes.tofile

Python 数组与 C 数组一样精简。如图 2-1 所示,float值的array不保存完整的float实例,而只保存代表其机器值的打包字节——类似于 C 语言中的double数组。创建array时,您提供一个类型码,一个用于确定数组中每个项目存储的基础 C 类型的字母。例如,b是 C 中称为signed char的类型码,一个范围从-128 到 127 的整数。如果创建一个array('b'),那么每个项目将存储在一个字节中,并解释为整数。对于大量数字序列,这可以节省大量内存。Python 不会让您放入与数组类型不匹配的任何数字。

示例 2-19 展示了创建、保存和加载一个包含 1000 万个浮点随机数的数组。

示例 2-19. 创建、保存和加载大量浮点数的数组
>>> from array import array  # ①
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7)))  # ②
>>> floats[-1]  # ③
0.07802343889111107 >>> fp = open('floats.bin', 'wb')
>>> floats.tofile(fp)  # ④
>>> fp.close()
>>> floats2 = array('d')  # ⑤
>>> fp = open('floats.bin', 'rb')
>>> floats2.fromfile(fp, 10**7)  # ⑥
>>> fp.close()
>>> floats2[-1]  # ⑦
0.07802343889111107 >>> floats2 == floats  # ⑧
True

导入array类型。

从任何可迭代对象(在本例中是生成器表达式)创建双精度浮点数(类型码'd')的数组。

检查数组中的最后一个数字。

将数组保存到二进制文件。

创建一个空的双精度数组。

从二进制文件中读取 1000 万个数字。

检查数组中的最后一个数字。

验证数组内容是否匹配。

如您所见,array.tofilearray.fromfile非常易于使用。如果尝试示例,您会注意到它们也非常快速。一个快速实验显示,array.fromfile从使用array.tofile创建的二进制文件中加载 1000 万个双精度浮点数大约需要 0.1 秒。这几乎比从文本文件中读取数字快 60 倍,后者还涉及使用内置的float解析每一行。使用array.tofile保存的速度大约比在文本文件中每行写一个浮点数快七倍。此外,具有 1000 万个双精度浮点数的二进制文件的大小为 80000000 字节(每个双精度浮点数 8 字节,零开销),而相同数据的文本文件大小为 181515739 字节。

对于表示二进制数据的数字数组的特定情况,例如光栅图像,Python 中有bytesbytearray类型,详见第四章。

我们通过表 2-3 总结了数组部分,比较了listarray.array的特性。

表 2-3。listarray中找到的方法和属性(为简洁起见,省略了已弃用的数组方法和对象也实现的方法)

列表 数组
s.__add__(s2) s + s2—连接
s.__iadd__(s2) s += s2—原地连接
s.append(e) 在最后一个元素后追加一个元素
s.byteswap() 交换数组中所有项目的字节以进行字节顺序转换
s.clear() 删除所有项目
s.__contains__(e) e in s
s.copy() 列表的浅拷贝
s.__copy__() 支持copy.copy
s.count(e) 计算元素的出现次数
s.__deepcopy__() 优化支持copy.deepcopy
s.__delitem__(p) 移除位置p处的项目
s.extend(it) 从可迭代对象it中追加项目
s.frombytes(b) 从字节序列中解释为打包的机器值追加项目
s.fromfile(f, n) 从解释为打包的机器值的二进制文件f追加n个项目
s.fromlist(l) 从列表追加项目;如果一个导致TypeError,则不追加任何项目
s.__getitem__(p) s[p]—获取位置处的项目或切片
s.index(e) 查找e的第一个出现位置
s.insert(p, e) 在位置p的项目之前插入元素e
s.itemsize 每个数组项的字节长度
s.__iter__() 获取迭代器
s.__len__() len(s)—项目数
s.__mul__(n) s * n—重复连接
s.__imul__(n) s *= n—原地重复连接
s.__rmul__(n) n * s—反向重复连接^(a)
s.pop([p]) 移除并返回位置p处的项目(默认为最后一个)
s.remove(e) 通过值删除元素e的第一个出现
s.reverse() 原地反转项目的顺序
s.__reversed__() 获取从最后到第一个扫描项目的迭代器
s.__setitem__(p, e) s[p] = e—将e放在位置p,覆盖现有项目或切片
s.sort([key], [reverse]) 使用可选关键字参数keyreverse原地对项目进行排序
s.tobytes() bytes对象的形式返回打包的机器值
s.tofile(f) 将项目保存为打包的机器值到二进制文件f
s.tolist() list中的数值对象形式返回项目
s.typecode 用于标识项目的 C 类型的单字符字符串
^(a) 反向运算符在 第十六章 中有解释。
提示

截至 Python 3.10,array 类型没有像 list.sort() 那样的原地 sort 方法。如果需要对数组进行排序,请使用内置的 sorted 函数重新构建数组:

a = array.array(a.typecode, sorted(a))

要在向数组添加项目时保持已排序数组的排序,请使用 bisect.insort 函数。

如果您经常使用数组并且不了解 memoryview,那么您会错过很多。请看下一个主题。

内存视图

内置的 memoryview 类是一个共享内存序列类型,允许您处理数组的切片而无需复制字节。它受到 NumPy 库的启发(我们将在 “NumPy” 中讨论)。NumPy 的首席作者 Travis Oliphant 对于何时应该使用 memoryview 的问题的回答是这样的:“何时应该使用 memoryview?”

内存视图本质上是 Python 中的一个广义 NumPy 数组结构(不涉及数学)。它允许您在不复制字节的情况下在数据结构之间共享内存(例如 PIL 图像、SQLite 数据库、NumPy 数组等)。这对于大型数据集非常重要。

使用类似于 array 模块的符号,memoryview.cast 方法允许您更改多个字节的读取或写入方式,而无需移动位。memoryview.cast 总是返回另一个共享相同内存的 memoryview 对象。

示例 2-20 展示了如何在相同的 6 个字节数组上创建替代视图,以便将其视为 2×3 矩阵或 3×2 矩阵进行操作。

示例 2-20. 将 6 个字节的内存处理为 1×6、2×3 和 3×2 视图
>>> from array import array
>>> octets = array('B', range(6))  # ①
>>> m1 = memoryview(octets)  # ②
>>> m1.tolist()
[0, 1, 2, 3, 4, 5] >>> m2 = m1.cast('B', [2, 3])  # ③
>>> m2.tolist()
[[0, 1, 2], [3, 4, 5]] >>> m3 = m1.cast('B', [3, 2])  # ④
>>> m3.tolist()
[[0, 1], [2, 3], [4, 5]] >>> m2[1,1] = 22  # ⑤
>>> m3[1,1] = 33  # ⑥
>>> octets  # ⑦
array('B', [0, 1, 2, 33, 22, 5])

构建包含 6 个字节的数组(类型码为 'B')。

从该数组构建 memoryview,然后将其导出为列表。

从先前的 memoryview 创建新的 memoryview,但具有 2 行和 3 列。

另一个 memoryview,现在有 3 行和 2 列。

m2 的第 1 行、第 1 列覆盖字节为 22

m3 的第 1 行、第 1 列覆盖字节为 33

显示原始数组,证明内存在 octetsm1m2m3 之间共享。

memoryview 的强大之处也可以用来损坏。示例 2-21 展示了如何更改 16 位整数数组中一个项目的单个字节。

示例 2-21. 通过修改一个字节来更改 16 位整数数组项的值
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers)  # ①
>>> len(memv)
5 >>> memv[0]  # ②
-2 >>> memv_oct = memv.cast('B')  # ③
>>> memv_oct.tolist()  # ④
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0] >>> memv_oct[5] = 4  # ⑤
>>> numbers
array('h', [-2, -1, 1024, 1, 2]) # ⑥

从包含 5 个 16 位有符号整数的数组(类型码为 'h')构建 memoryview

memv 在数组中看到相同的 5 个项目。

通过将 memv 的元素转换为字节(类型码为 'B')来创建 memv_oct

memv_oct 的元素导出为包含 10 个字节的列表,以供检查。

将值 4 分配给字节偏移 5

注意 numbers 的变化:2 字节无符号整数的最高有效字节中的 41024

注意

您将在 fluentpython.com 上找到使用 struct 包检查 memoryview 的示例:“使用 struct 解析二进制记录”

同时,如果您在数组中进行高级数值处理,应该使用 NumPy 库。我们将立即简要介绍它们。

NumPy

在本书中,我强调了 Python 标准库中已经存在的内容,以便您能充分利用它。但是 NumPy 如此强大,值得一提。

对于高级的数组和矩阵操作,NumPy 是 Python 在科学计算应用中变得流行的原因。NumPy 实现了多维、同质数组和矩阵类型,不仅保存数字,还保存用户定义的记录,并提供高效的逐元素操作。

SciPy 是一个库,建立在 NumPy 之上,提供许多来自线性代数、数值微积分和统计学的科学计算算法。SciPy 快速可靠,因为它利用了来自Netlib Repository的广泛使用的 C 和 Fortran 代码库。换句话说,SciPy 为科学家提供了最佳的两种选择:交互式提示符和高级 Python API,以及在 C 和 Fortran 中优化的工业强度数值计算函数。

作为一个非常简短的 NumPy 演示,示例 2-22 展示了一些关于二维数组的基本操作。

示例 2-22。在numpy.ndarray中进行行和列的基本操作
>>> import numpy as np # ①
>>> a = np.arange(12)  # ②
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11]) >>> type(a)
<class 'numpy.ndarray'> >>> a.shape  # ③
(12,) >>> a.shape = 3, 4  # ④
>>> a
array([[ 0,  1,  2,  3],
 [ 4,  5,  6,  7], [ 8,  9, 10, 11]]) >>> a[2]  # ⑤
array([ 8,  9, 10, 11]) >>> a[2, 1]  # ⑥
9 >>> a[:, 1]  # ⑦
array([1, 5, 9]) >>> a.transpose()  # ⑧
array([[ 0,  4,  8],
 [ 1,  5,  9], [ 2,  6, 10], [ 3,  7, 11]])

导入 NumPy,在安装后(不在 Python 标准库中)。按照惯例,将numpy导入为np

构建并检查一个包含整数011numpy.ndarray

检查数组的维度:这是一个一维的,包含 12 个元素的数组。

改变数组的形状,增加一个维度,然后检查结果。

获取索引为2的行。

获取索引为2, 1的元素。

获取索引为1的列。

通过转置(交换列和行)创建一个新的数组。

NumPy 还支持用于加载、保存和操作numpy.ndarray的高级操作:

>>> import numpy
>>> floats = numpy.loadtxt('floats-10M-lines.txt')  # ①
>>> floats[-3:]  # ②
array([ 3016362.69195522,   535281.10514262,  4566560.44373946]) >>> floats *= .5  # ③
>>> floats[-3:]
array([ 1508181.34597761,   267640.55257131,  2283280.22186973]) >>> from time import perf_counter as pc # ④
>>> t0 = pc(); floats /= 3; pc() - t0 # ⑤
0.03690556302899495 >>> numpy.save('floats-10M', floats)  # ⑥
>>> floats2 = numpy.load('floats-10M.npy', 'r+')  # ⑦
>>> floats2 *= 6
>>> floats2[-3:]  # ⑧
memmap([ 3016362.69195522,   535281.10514262,  4566560.44373946])

从文本文件中加载 1000 万个浮点数。

使用序列切片表示法检查最后三个数字。

floats数组中的每个元素乘以.5,然后再次检查最后三个元素。

导入高分辨率性能测量计时器(自 Python 3.3 起可用)。

将每个元素除以3;对于 1000 万个浮点数,经过的时间不到 40 毫秒。

将数组保存为*.npy*二进制文件。

将数据作为内存映射文件加载到另一个数组中;这允许对数组的切片进行高效处理,即使它不能完全放入内存中。

将每个元素乘以6后,检查最后三个元素。

这只是一个开胃菜。

NumPy 和 SciPy 是强大的库,是其他出色工具的基础,比如 Pandas — 实现了可以容纳非数值数据的高效数组类型,并提供了许多不同格式的导入/导出功能,如 .csv.xls、SQL dumps、HDF5 等 — 以及 scikit-learn,目前是最广泛使用的机器学习工具集。大多数 NumPy 和 SciPy 函数是用 C 或 C++ 实现的,并且可以利用所有 CPU 核心,因为它们释放了 Python 的 GIL(全局解释器锁)。Dask 项目支持在机器群集上并行处理 NumPy、Pandas 和 scikit-learn。这些包值得写一整本书来介绍。但这不是那本书。但是,没有至少简要介绍 NumPy 数组的 Python 序列概述是不完整的。

在查看了平面序列 — 标准数组和 NumPy 数组之后,我们现在转向一组完全不同的替代品,用于替代普通的 list:队列。

Deques 和其他队列

.append.pop 方法使得 list 可以用作堆栈或队列(如果使用 .append.pop(0),则获得 FIFO 行为)。但是,在列表头部(0 索引端)插入和删除是昂贵的,因为整个列表必须在内存中移动。

collections.deque 是一个线程安全的双端队列,旨在快速从两端插入和删除。如果需要保留“最近看到的项目”列表或类似内容,deque 也是一个不错的选择,因为 deque 可以是有界的 — 即,创建时具有固定的最大长度。如果有界 deque 已满,在添加新项目时,它会从相反端丢弃一个项目。示例 2-23 展示了在 deque 上执行的一些典型操作。

示例 2-23. 使用 deque
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10)  # ①
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10) >>> dq.rotate(3)  # ②
>>> dq
deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10) >>> dq.rotate(-4)
>>> dq
deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10) >>> dq.appendleft(-1)  # ③
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10) >>> dq.extend([11, 22, 33])  # ④
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10) >>> dq.extendleft([10, 20, 30, 40])  # ⑤
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)

可选的 maxlen 参数设置了此 deque 实例中允许的最大项目数;这将设置一个只读的 maxlen 实例属性。

使用 n > 0 旋转会从右端获取项目并将其前置到左端;当 n < 0 时,项目从左端获取并附加到右端。

向已满的 dequelen(d) == d.maxlen)添加元素会丢弃另一端的项目;请注意下一行中的 0 被丢弃了。

向右侧添加三个项目会推出最左侧的 -112

请注意,extendleft(iter) 的工作方式是将 iter 参数的每个连续项目附加到 deque 的左侧,因此项目的最终位置是反转的。

Table 2-4 比较了 listdeque 中特定的方法(删除了也出现在 object 中的方法)。

请注意,deque 实现了大多数 list 方法,并添加了一些特定于其设计的方法,如 popleftrotate。但是存在隐藏成本:从 deque 中间删除项目不够快。它真正优化于从两端附加和弹出。

appendpopleft 操作是原子性的,因此在多线程应用程序中,deque 可以安全地用作 FIFO 队列,无需使用锁。

表 2-4. 在 listdeque 中实现的方法(省略了那些也由 object 实现的方法)

list deque
s.__add__(s2) s + s2—连接
s.__iadd__(s2) s += s2—原地连接
s.append(e) 向右侧附加一个元素(在最后之后)
s.appendleft(e) 向左侧附加一个元素(在第一个之前)
s.clear() 删除所有项目
s.__contains__(e) e in s
s.copy() 列表的浅复制
s.__copy__() 支持 copy.copy(浅复制)
s.count(e) 计算元素出现的次数
s.__delitem__(p) 删除位置 p 处的项目
s.extend(i) 将可迭代对象 i 中的项目添加到右侧
s.extendleft(i) 将可迭代对象 i 中的项目添加到左侧
s.__getitem__(p) s[p]—获取位置处的项目或切片
s.index(e) 查找第一个出现的 e 的位置
s.insert(p, e) 在位置 p 的项目之前插入元素 e
s.__iter__() 获取迭代器
s.__len__() len(s)—项目数量
s.__mul__(n) s * n—重复连接
s.__imul__(n) s *= n—原地重复连接
s.__rmul__(n) n * s—反向重复连接
s.pop() 移除并返回最后一个项目
s.popleft() 移除并返回第一个项目
s.remove(e) 按值删除第一个出现的元素 e
s.reverse() 原地反转项目顺序
s.__reversed__() 获取迭代器以从后向前扫描项目
s.rotate(n) n 个项目从一端移动到另一端
s.__setitem__(p, e) s[p] = e—将 e 放在位置 p,覆盖现有的项目或切片
s.sort([key], [reverse]) 使用可选关键字参数 keyreverse 原地对项目进行排序
^(a) 反向操作符在 第十六章 中有解释。^(b) a_list.pop(p) 允许从位置 p 处移除项目,但 deque 不支持该选项。

除了 deque,其他 Python 标准库包实现了队列:

queue

这提供了同步(即线程安全)的类 SimpleQueueQueueLifoQueuePriorityQueue。这些可以用于线程之间的安全通信。除了 SimpleQueue 外,通过向构造函数提供大于 0 的 maxsize 参数,其他队列都可以被限制大小。然而,它们不会像 deque 那样丢弃项目以腾出空间。相反,当队列已满时,插入新项目会被阻塞—即等待直到其他线程通过从队列中取出项目来腾出空间,这对于限制活动线程数量很有用。

multiprocessing

实现了自己的无界 SimpleQueue 和有界 Queue,与 queue 包中的类非常相似,但设计用于进程间通信。提供了专门的 multiprocessing.JoinableQueue 用于任务管理。

asyncio

提供了受 queuemultiprocessing 模块中类启发的 QueueLifoQueuePriorityQueueJoinableQueue,但适用于管理异步编程中的任务。

heapq

与前三个模块不同,heapq 不实现队列类,而是提供函数如 heappushheappop,让您可以使用可变序列作为堆队列或优先队列。

这结束了我们对 list 类型的替代品以及对序列类型的一般探索—除了 str 和二进制序列的细节,它们有自己的章节(第四章)。

章节总结

精通标准库的序列类型是编写简洁、高效和惯用的 Python 代码的先决条件。

Python 序列通常被归类为可变或不可变,但考虑一个不同的维度也是有用的:扁平序列和容器序列。前者更紧凑、更快速、更易于使用,但仅限于存储数字、字符和字节等原子数据。容器序列更灵活,但当它们持有可变对象时可能会让您感到惊讶,因此您需要小心地在嵌套数据结构中正确使用它们。

不幸的是,Python 没有绝对可靠的不可变容器序列类型:即使“不可变”元组中包含可变项(如列表或用户定义对象),其值也可能被更改。

列表推导和生成器表达式是构建和初始化序列的强大表示法。如果您尚未熟悉它们,请花时间掌握它们的基本用法。这并不难,很快您就会上瘾。

在 Python 中,元组扮演两个角色:作为具有未命名字段的记录和作为不可变列表。当将元组用作不可变列表时,请记住,仅当其中所有项也是不可变时,元组值才被保证固定。在元组上调用 hash(t) 是一种快速断言其值固定的方法。如果 t 包含可变项,则会引发 TypeError

当元组用作记录时,元组解包是提取元组字段的最安全、最可读的方式。除了元组外,* 在许多上下文中与列表和可迭代对象一起使用,并且在 Python 3.5 中出现了一些用例,其中包括 PEP 448—Additional Unpacking Generalizations。Python 3.10 引入了带有 match/case 的模式匹配,支持更强大的解包,称为解构。

序列切片是 Python 中一个受欢迎的语法特性,比许多人意识到的要更强大。多维切片和省略号(...)符号,如 NumPy 中使用的方式,也可能受到用户定义序列的支持。对切片赋值是编辑可变序列的一种非常表达性的方式。

seq * n 中的重复连接很方便,并且经过小心处理,可以用于初始化包含不可变项的列表列表。对于可变和不可变序列,使用 +=*= 的增强赋值行为不同。在后一种情况下,这些运算符必然构建新序列。但如果目标序列是可变的,则通常会就地更改它,但并非总是,这取决于序列的实现方式。

sort 方法和 sorted 内置函数易于使用且灵活,这要归功于可选的 key 参数:用于计算排序标准的函数。顺便说一句,key 也可以与 minmax 内置函数一起使用。

除了列表和元组外,Python 标准库还提供了 array.array。虽然 NumPy 和 SciPy 不是标准库的一部分,但如果您对大量数据进行任何类型的数值处理,学习这些库的一小部分甚至可以让您走得更远。

我们最后讨论了多才多艺且线程安全的 collections.deque,将其 API 与 list 在表 2-4 中进行了比较,并提到了标准库中的其他队列实现。

进一步阅读

第一章“数据结构”来自Python Cookbook,第 3 版(O’Reilly),作者是 David Beazley 和 Brian K. Jones,其中包含许多关于序列的技巧,包括“Recipe 1.11. 命名切片”,我从中学到了将切片赋值给变量以提高可读性的技巧,在我们的示例 2-13 中有所展示。

Python Cookbook 的第二版是为 Python 2.4 编写的,但其中的许多代码也适用于 Python 3,并且第五章和第六章中的许多技巧涉及序列。该书由 Alex Martelli、Anna Ravenscroft 和 David Ascher 编辑,其中包括数十位 Python 爱好者的贡献。第三版是从头开始重写的,更侧重于语言的语义,特别是 Python 3 中发生了什么变化,而旧版则更强调实用性(即如何将语言应用于实际问题)。尽管第二版的一些解决方案不再是最佳方法,但我真诚地认为值得同时拥有Python Cookbook 的两个版本。

官方 Python “排序 HOW TO” 中有几个关于使用 sortedlist.sort 的高级技巧示例。

PEP 3132—扩展可迭代解包是阅读关于在并行赋值的左侧使用*extra语法的新用法的权威来源。如果你想一窥 Python 的发展,“缺失*-解包泛化”是一个提出增强可迭代解包符号的 bug 跟踪器问题。PEP 448—额外解包泛化是从该问题的讨论中产生的。

正如我在“使用序列进行模式匹配”中提到的,Carol Willing 的“结构化模式匹配”部分在“Python 3.10 有什么新特性”中是对这一重要新功能的很好介绍,大约有 1400 字(当 Firefox 从 HTML 生成 PDF 时,这不到 5 页)。PEP 636—结构化模式匹配:教程也不错,但更长。同样的 PEP 636 包括“附录 A—快速介绍”。它比 Willing 的介绍短,因为它省略了关于为什么模式匹配对你有好处的高层考虑。如果你需要更多论据来说服自己或他人模式匹配对 Python 有好处,那么阅读 22 页的PEP 635—结构化模式匹配:动机和原理

Eli Bendersky 的博客文章“使用缓冲区协议和 memoryviews 在 Python 中减少拷贝”包含了关于memoryview的简短教程。

市场上有许多涵盖 NumPy 的书籍,许多书名中并未提及“NumPy”。两个例子是 Jake VanderPlas 的开放获取书籍Python 数据科学手册,以及 Wes McKinney 的第二版Python 数据分析

“NumPy 的全部内容都关乎向量化。”这是 Nicolas P. Rougier 的开放获取书籍从 Python 到 NumPy的开篇语句。向量化操作将数学函数应用于数组的所有元素,而无需在 Python 中编写显式循环。它们可以并行操作,使用现代 CPU 中的特殊向量指令,利用多个核心或委托给 GPU,具体取决于库。Rougier 的书中的第一个例子展示了通过将一个漂亮的 Python 类使用生成器方法重构为调用几个 NumPy 向量函数的精简函数后,速度提高了 500 倍。

要学习如何使用deque(以及其他集合),请参阅 Python 文档中“容器数据类型”中的示例和实用配方。

Python 惯例中排除范围和切片中的最后一项的最佳辩护是由 Edsger W. Dijkstra 亲自撰写的,标题为“为什么编号应该从零开始”的短备忘录。备忘录的主题是数学符号,但与 Python 相关,因为 Dijkstra 以严谨和幽默解释了为什么像 2, 3, …, 12 这样的序列应该始终表示为 2 ≤ i < 13。所有其他合理的惯例都被驳斥,以及让每个用户选择惯例的想法。标题指的是基于零的索引,但备忘录实际上是关于为什么'ABCDE'[1:3]意味着'BC'而不是'BCD',以及为什么写range(2, 13)来生成 2, 3, 4, …, 12 是完全合理的。顺便说一句,备忘录是一张手写的便条,但它非常漂亮且完全可读。Dijkstra 的笔迹非常清晰,以至于有人根据他的笔记创建了一个字体

¹ Leo Geurts,Lambert Meertens 和 Steven Pemberton,ABC 程序员手册,第 8 页。 (Bosko Books)。

² 感谢读者 Tina Lapine 指出这一点。

³ 感谢技术审阅员 Leonardo Rochael 提供此示例。

⁴ 在我看来,一系列的if/elif/elif/.../else块是对switch/case的一个很好的替代。它不会受到一些语言设计者在几十年后仍然无谓地从 C 语言中复制的贯穿悬空 else问题的困扰,这些问题已经被广泛认为是导致无数错误的原因。

⁵ 后者在 Norvig 的代码中被命名为eval;我将其重命名以避免与 Python 的eval内置函数混淆。

⁶ 在“内存视图”中,我们展示了特别构造的内存视图可以具有多个维度。

⁷ 不,我没有搞错:ellipsis类名确实全小写,而实例是一个名为Ellipsis的内置对象,就像bool是小写但其实例是TrueFalse一样。

str是这个描述的一个例外。因为在实际代码库中,在循环中使用+=进行字符串构建是如此普遍,CPython 对这种用例进行了优化。str的实例在内存中分配了额外的空间,因此连接不需要每次都复制整个字符串。

⁹ 感谢 Leonardo Rochael 和 Cesar Kawakami 在 2013 年 PythonBrasil 大会上分享这个谜题。

¹⁰ 读者建议在示例中的操作可以用t[2].extend([50,60])来完成,而不会出错。我知道这一点,但我的目的是展示在这种情况下+=运算符的奇怪行为。

¹¹ 接收者是方法调用的目标,是方法体中绑定到self的对象。

¹² Python 的主要排序算法以其创造者 Tim Peters 命名为 Timsort。有关 Timsort 的一些趣闻,参见“讲台”。

¹³ 这个例子中的单词按字母顺序排序,因为它们完全由小写 ASCII 字符组成。请参见示例后的警告。

¹⁴ 先进先出——队列的默认行为。

相关文章
|
13天前
|
存储 Python
流畅的 Python 第二版(GPT 重译)(十)(2)
流畅的 Python 第二版(GPT 重译)(十)
21 0
|
13天前
|
设计模式 算法 程序员
流畅的 Python 第二版(GPT 重译)(五)(3)
流畅的 Python 第二版(GPT 重译)(五)
35 2
|
13天前
|
存储 API 芯片
流畅的 Python 第二版(GPT 重译)(九)(2)
流畅的 Python 第二版(GPT 重译)(九)
63 1
|
13天前
|
安全 程序员 API
流畅的 Python 第二版(GPT 重译)(一)(1)
流畅的 Python 第二版(GPT 重译)(一)
97 5
|
13天前
|
存储 API 数据库
流畅的 Python 第二版(GPT 重译)(一)(2)
流畅的 Python 第二版(GPT 重译)(一)
53 4
|
13天前
|
JSON JavaScript Java
流畅的 Python 第二版(GPT 重译)(三)(3)
流畅的 Python 第二版(GPT 重译)(三)
45 5
|
13天前
|
存储 测试技术 Python
流畅的 Python 第二版(GPT 重译)(九)(3)
流畅的 Python 第二版(GPT 重译)(九)
29 0
|
13天前
|
存储 Java 测试技术
流畅的 Python 第二版(GPT 重译)(四)(1)
流畅的 Python 第二版(GPT 重译)(四)
48 1
|
JavaScript 物联网 Linux
流畅的 Python 第二版(GPT 重译)(二)(3)
流畅的 Python 第二版(GPT 重译)(二)
61 4
|
安全 测试技术 程序员
流畅的 Python 第二版(GPT 重译)(三)(2)
流畅的 Python 第二版(GPT 重译)(三)
31 11

热门文章

最新文章