切片
list、tuple、str以及其它序列类型的具有一个共同的功能,那就是对切片运算的支持,这一功能比很多人所认知的要更为强大。
本节我们讲解切片高级形式的用法。其在自定义类中的实现将在系列十二中进行讲解。
为何切片和 range 或排除最后一项
切片和 range排除最后一项这一Pythonic惯例适用于Python、C和其它以0为初始索引的编程语言。这一惯例提供下述便利:
-
在仅提供结束位置时易于查看切片或range的长度:
range(3)和my_list[:3]都生成三项。 -
在提供了开始(start)和结束(stop)位置时易于计算切片或range的长度:只需使用
stop - start。 -
易于在任意索引
x处将序列分成两部分而不会存在重叠:即my_list[:x]和my_list[x:]。例如:>>> l = [10, 20, 30, 40, 50, 60] >>> l[:2] # split at 2 [10, 20] >>> l[2:] [30, 40, 50, 60] >>> l[:3] # split at 3 [10, 20, 30] >>> l[3:] [40, 50, 60]
关于这一惯例的最佳论证由荷兰计算机科学家Edsger W. Dijkstra所写(参见扩展阅读部分)。
下面我们来仔细学习Python是如何解析切片标记的。
切片对象
这可能已经是常识,但有必要重复说明下:s[a:b:c]可用于指定步长c,使用得结果切片跳过指定数量的元素。步长也可为负数,此时逆向返回各元素。举三个例子来说明:
>>> s = 'bicycle'
>>> s[::3]
'bye'
>>> s[::-1]
'elcycib'
>>> s[::-2]
'eccb'
还有一个示例是在系列一中我们使用了deck[12::13]在尚未洗牌时获取所有的“老A”:
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
a:b:c这种写法仅在索引或下标运算时放在[]中有效,产生一个切片对象slice(a, b, c)。在系列十二的切片如何运行中会讲到,运算seq[start:stop:step]表达式,Python会调用seq.__getitem__(slice(start, stop, step))。即使不自己实现序列类型,了解切片对象也是有益的,因为这样我们可以对切片命名,就像是在 Excel 中对一组单元格命名。
假设需求解析例2-13中所示这种普通文本数据发票。我们可以不在代码中对切片进行硬编码,而是对其进行命名。参见在下面的for循环中如何增强了其可读性。
示例2-13: 取自普通发票的几行数据
>>> invoice = """
... 0.....6.................................40........52...55........
... 1909 Pimoroni PiBrella $17.50 3 $52.50
... 1489 6mm Tactile Switch x20 $4.95 2 $9.90
... 1510 Panavise Jr. - PV-201 $28.00 1 $28.00
... 1601 PiTFT Mini Kit 320x240 $34.95 1 $34.95
... """
>>> SKU = slice(0, 6)
>>> DESCRIPTION = slice(6, 40)
>>> UNIT_PRICE = slice(40, 52)
>>> QUANTITY = slice(52, 55)
>>> ITEM_TOTAL = slice(55, None)
>>> line_items = invoice.split('\n')[2:]
>>> for item in line_items:
... print(item[UNIT_PRICE], item[DESCRIPTION])
...
$17.50 Pimoroni PiBrella
$4.95 6mm Tactile Switch x20
$28.00 Panavise Jr. - PV-201
$34.95 PiTFT Mini Kit 320x240
在系列十二的向量#2:可进行切片的序列中讨论创建自己的集合时我们还会讲slice对象。同时从用户视角看,切片包含一些额外的功能,如多维切片以及展开符(...)写法。
多维切片与展开运算符
[]运算符还可接收以逗号分隔的多个索引或切片。处理[]运算的专有方法__getitem__和__setitem__会a[i, j]中的索引按元组进行接收。也即运算a[i, j]时,Python调用的是a.__getitem__((i, j))。
在外部包NumPy中就有使用,可使用a[i, j]来获取二维的numpy.ndarray、使用a[m:n, k:l]获取二维切片。本文稍后的例2-22有这种用法。
除memoryview外,Python内置的序列类型都是一维的,因此均只支持一个索引或切片,无法使用它样的元组。
展开运算符使用三个点(...) ,而非普通的省略号…(Unicode U+2026),Python解析器会将其识别为一个令牌。它是Ellipsis对象的别名,也是ellipsis类的唯一实例。因此,其可以参数传递给函数或切片定义时使用,如f(a, ..., z)或a[i:...]。NumPy在获取多维数组切片时使用...作为一种简写方式,例如,假定x是四维数组,x[i, ...]就是x[i, :, :, :,]的简写。参见NumPy快速起步进行更详细的了解。
在写本文时,Python标准库中尚未有Ellipsis或多维索引及切片的用法。读者如果发现有,欢迎在评论区告知。这类语法特性用于支持自定义类型及NumPy这样的扩展。
切片不仅可用于提供序列中的信息,还可以原地修改可变序列,无需重新进行构建。
对切片赋值
可变序列可以使用切片符号进行切割、删除以及原地修改,这一符号可用在赋值语句左侧或作为del语句的目标。下面的例子演示了切片符号的强大之处:
>>> l = list(range(10))
>>> l
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> l[2:5] = [20, 30]
>>> l
[0, 1, 20, 30, 5, 6, 7, 8, 9]
>>> del l[5:7]
>>> l
[0, 1, 20, 30, 5, 8, 9]
>>> l[3::2] = [11, 22]
>>> l
[0, 1, 20, 11, 5, 22, 9]
>>> l[2:5] = 100 # 赋值的目标方是切片时,即便只有一个元素右值也必须是可迭代对象。
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only assign an iterable
>>> l[2:5] = [100]
>>> l
[0, 1, 100, 22, 9]
每个工程师都知道拼接是序列的常规操作。这时Python入门教程中都会讲解到+和*的用法,但两者的运行存在一些细节,下一部分中会进行讲解。
对序列使用+和*
Python程序员默认序列支持+和*。通常+的两边应当为相同序列类型,两者都不会被修改,而是新建一个同类型的序列作为拼接的结果。
要对相同序列的多个拷贝进行拼接,可使用一个整数乘上该序列。同样,这也会新建一个序列:
>>> l = [1, 2, 3]
>>> l * 5
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 5 * 'abcd'
'abcdabcdabcdabcdabcd'
+和*总是新建对象,而不对运算项进行修改。
警告: 在使用a * n且其中的a为包含可变子项的序列时要格外小心,因为结果会惊掉你的下巴。例如,使用my_list = [[]] * 3来初始化列表时会产生一个对同一列表的三个引用,这可能不是你预期的结果。
下一节会讲解使用*初始化列表的列表时存在的坑。
构建列表的列表
有时我们需要使用一定数量的内嵌列表初始化列表,例如,将学生分到一组队伍里或表示游戏板中方块。实现的最佳方式是列表推导式,参见示例2-14.
示例2-14:一个由3组3元列表组成的列表可用于表示井字棋
>>> board = [['_'] * 3 for i in range(3)] # 创建3个3元列表组成的列表。查看结构
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X' # 在第1行第2列放入一个标记,查看结果
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]
大家经常容易走示例2-15这种错误的捷径。
示例2-15: 指向同一列表的三个引用的列表毫无意义
>>> weird_board = [['_'] * 3] * 3 # 外部的列表由三个都指向同一内层列表的引用组成。在不进行修改时看不出异常。
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O' # 在第1行第2列放入标记后,会发现所有行都是引用同一对象的别名。
>>> weird_board
[['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]
示例2-15的问题是本质其类似如下代码:
row = ['_'] * 3
board = []
for i in range(3):
board.append(row) # 同一个row连续3次添加到board中
而示例2-14中的列表推导式等价于如下代码:
>>> board = []
>>> for i in range(3):
... row = ['_'] * 3 # 每次迭代会新建一个row并添加至board
... board.append(row)
...
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[2][0] = 'X'
>>> board # 和预想的一样,只有行号为2的进行了修改
[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']]
小贴士: 如果这部分中的问题或解决方案你看不懂,不必担心。系列六中会讲解引用和可变对象的机制及缺点。
至此我们讨论了对序列使用+和*运算符,但还有+=和*=运算符,根据目标序列的可变性会产生截然不同的结果。下一节会讲解个中原理。
序列的增量赋值
增量赋值运算符+=和*=根据这个操作数的不同行为也不同。为便于讨论,我们先主要讲增量加(+=),但其原理同样适用*=及其它增量赋值运算符。
+=背后的特殊方法是__iadd__(用于原地加)。但如果未实现__iadd__,Python会自动降级至调用__add__。思考如下的简单表达式:
>>> a += b
如果a实现了__iadd__,就会调用该方法。对于可变序列(如list、bytearray、array.array),a会在原地改变(即效果类似于a.extend(b))。但在如果a没有实现__iadd__时,a += b表达式和a = a + b具有同等效果:先计算a + b表达式,生成新的对象,然后与a进行绑定。换句话说,绑定a的对象内存地址根据__iadd__的可用性会改变或不变。
对于可变序列,一般都实现了__iadd_,通常可假定+=为原地修改。对于不可变序列显示不可能做原地修改。
以上有关+=的讨论同样适用于*=,由__imul__进行实现。__iadd__和__imul__专用方法会在系列十六中进行讨论。
下面是*=对可变序列以及不可变序列的演示:
>>> l = [1, 2, 3]
>>> id(l)
4311953800 # 初始列表的
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
4311953800 # 相乘后,列表为新增了元素后的同一对象
>>> t = (1, 2, 3)
>>> id(t)
4312681568 # 初始元组的ID
>>> t *= 2
>>> id(t)
4301348296 # 相乘后,新建了一个元组
对不可变序列反复拼接效率很低,因为它并不是直接添加元素,而是由解释器拷贝整个对象创建一个拼接了新元素的新对象。
我们已经学习了+=的常用用法。下一节通过有趣的极端案例讲解对于元组“不可变”是什么意思。
A +=赋值迷题
试着不用控制台操作先回答这个问题: 例2-16中的两个表达式的运算结果是什么?
示例2-16:猜一猜
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
接下来会发生什么?选择最佳答案:
A. t变为(1, 2, [30, 40, 50, 60])。
B. 抛出错误消息为'tuple' object does not support item assignment的TypeError。
C. A 和 B都不对。
D. 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在线课堂是一个用于详细可视化Python运行原理的很棒的工具。图2-5由两张截图组成,演示了示例2-17中元组t的初始状态和最终状态。
图2-5:元组赋值迷题的初始和最终状态(该图由 Python 在线课堂生成)
如果查看Python为s[a] += b表达式生成的字节码(例2-18),背后发生了什么会变得清晰。
示例2-18:s[a] += b表达式的字节码
>>> dis.dis('s[a] += b')
1 0 LOAD_NAME 0 (s)
2 LOAD_NAME 1 (a)
4 DUP_TOP_TWO
6 BINARY_SUBSCR # 将s[a]的值放在栈顶(TOS)
8 LOAD_NAME 2 (b)
10 INPLACE_ADD # 执行TOS += b。如果TOS指向可变对象则成功(例2-17中为列表)
12 ROT_THREE
14 STORE_SUBSCR # 赋值s[a] = TOS。如s为不可变则失败(例2-17中 t中为元组)
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
本例是一种极端的情况,在原作者20年的Python编程中,也未一例因此影响到实际程序的。
从这里面我学习到了三点:
- 不要将可变元素放到元组中
- 增量赋值并非原子运算-我们只看到执行部分任务后其抛出异常
- 查看Python字节码并不是太复杂,有助于了解底层的问题。
在学习了+和*拼接的细节后,我们可以进入另一个序列的基本运算:排序。
list.sort vs. 内置的sorted
list.sort方法在原处对列表进行排序,即不使用拷贝。其返回值为None,这告诉我们它修改了接收器且未新建列表。这是一个重要的Python API公约:原地修改对象的函数或方法应返回None,以让调用者清楚接收器发生了变化,且未创建新对象。例如可以在random.shuffle(s)函数中看到类似的行为,它在原地对可变序列s进行了随机排序并返回None。
注:返回None用于表示原地修改的公约有一个缺点:我们无法对这些方法进行级联调用。而返回新对象的方法(如所有str方法)可以流式接口进行调用,参见维基百科的流式接口词条中的详细描述。
与之对应的内置函数sorted新建列表并返回。它的参数可接收任意可迭代对象,包含不可变序列和生成器(见系列十七)。不论赋值给sorted的可迭代类型是什么,它总是返回一个新创建的列表。
list.sort和sorted都可接收两个可选的关键词参数:
reverse:若为True,返回的元素按降序排列(即元素按反向比较)。其默认值为False。key:对每个元素应用一个单参数函数,生成排序的键。例如,在对一个字符串列表进行排序时,key=str.lower可用于执行一个不区分大小写的排序,key=len会按字符长度对字符串排序。默认为id 函数(即对元素自身进行比较)。
小贴士:也可以对可选关键字参数key使用内置函数min()和max()以及其它标准库中的函数(如itertools.groupby()和heapq.nlargest())。
以下示例用于说明这些函数和关键字参数的用法。这些示例也演示了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'] # 按升序排序的新字符串列表。因排序算法是稳定的,grape和apple的长度都是5,保留了原始排序。
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple'] # 字符串按长度降序排列。它并不是前面结果的反转,因为排序是稳定的,所以grape还是在apple前面。
>>> fruits
['grape', 'raspberry', 'apple', 'banana'] # 至此原来的列表fruits未发生改变
>>> fruits.sort() # 这会在原地对列表排序,返回None(在控制台中会省去)
>>> fruits
['apple', 'banana', 'grape', 'raspberry'] # 此时fruits进行了排序
默认Python按字符编码的排序对字符串排序。这表示ASCII大写字母会在小写字母前,而非ASCII字符不会通识进行排序。系列四的对 Unicode 文本进行排序一节会讲解如何按人类通识进行排序。
序列进行排序后,可以进行高效搜索。在Python标准库的bisect模块中提供了二进制搜索算法。这个模块还包含bisect.insort函数,可用于确保排序的序列保持为排序状态。在配套网站 fluentpython.com的使用二分查找管理已排序序列有对bisect模块的讲解。
本文至此所学的都适用于所有序列,而不仅仅是列表或元组。Python工程师有时会过度使用list类型因其非常顺手,我就这和干过。例如在处理大数据量的列表时,应考虑使用数组。本文接下来会讲解列表和元组的一些替代品。
在列表并非答案时
list类型灵活易用,但有某些特殊要求时,还有更好的选择。例如,在处理数百万浮点数时array会节省大量的内存。另外,如果一直在列表的另一端添加、删除元素,deque(双端队列)是一个性能更好的先进先出数据结构。
小贴士:如果代码中需要经常查看某个元素是否在容器中(如item in my_collection),考虑对my_collection使用set,尤其是在存储大量元素时。集合对快速成员查找做了优化。也可对其进行迭代,但它不是序列,因为集合中元素的排序是不固定的。在系列三中会进行讲解。
本文接下来会讨论可替代列表的可变序列类型,先从数组开始。
数组
如果列表中仅包含数字,array.array是一个更高效的替代。数组支持所有的可变序列运算(包含.pop、.insert和.extend),也支持其它用于快速加载和保存的方法,如.frombytes和.tofile。
Python的数组和C的数组一样轻量。如图2-1所示,浮点值数组并不存储完整的float实例,而仅仅是表示其机器值的已封装字节,类似C语言中的double数组。在创建array时,提供一个表示C语言底层用于存储数组中元素数据类型的类型代码。例如,b是C中称为signed char的类型代码,该类型的值在–128到127之间。如若创建一个array('b'),每个元素会在单个字节中进行存储并被解释为整型。对于数字的大型序列,这会节约大量内存。Python不允许将不匹配数组类型的数字放到其中。
示例2-19演示了创建、保存和加载一个包含一千万随机浮点值的数组。
示例2-19:创建、保存及加载大型浮点数组
>>> from array import array # 导入array类型
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7))) # 通过任意迭代对象(此处为生成式表达式)创建一个双精度浮点值的数组(类型代码 'd')
>>> 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) # 从二进制文件中读取1000万个数字
>>> fp.close()
>>> floats2[-1] # 查看数组的最后一个数
0.07802343889111107
>>> floats2 == floats # 验证数组的内容是否一致
True
可以看出,array.tofile和array.fromfile使用起来看容易。如果测试本例,会发现运行很快。快速的实验表明array.fromfile从二进制文件加载1千万个双精度浮点值仅需0.1秒。它比从文本文件读取数字要快近60倍,那样会需要使用内置的float解析每一行。使用array.tofile进行保存比在文本文件中每行写一个浮点数要快7倍。此外,1千万个双精度值的二进制文件的大小为80,000,000字节(每个双精度值为8个字节,没有额外开销),而同样的数据文本文件要占到181,515,739字节。
具体表示二进制数据的数字数组,如栅格图像,Python有bytes和bytearray类型,在系列四中会进行讲解。
我们使用表2-3来结束数组这部分内容,其中对比了list和array.array的特性。
表2-3:列表或数组的方法和属性(为保持简洁省去了已淘汰的数组方法及对象中也实现了的方法)
| list | array | ||
|---|---|---|---|
| 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) | ● | 通过列表添加元素;如出现类型错误则都不会添加 | |
| 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—反向反复拼接 |
| 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]) | ● | 使用可选关键字参数key 和 reverse原地对元素排序 | |
| s.tobytes() | ● | 按字节对象以对齐机器值返回元素 | |
| s.tofile(f) | ● | 将元素以对齐机器值保存到二进制文件f中 | |
| s.tolist() | ● | 按列表以数字对象返回元素 | |
| s.typecode | ● | 标识元素的 C 语言类型的单字符字符串 |
小贴士:在Python 3.10中,数组类型并没有list.sort()这样的原地排序方法。如需对数组排序,使用内置的sorted函数重建数组:
a = array.array(a.typecode, sorted(a))在
在添加元素时如需保留数组排序,可使用bisect.insort函数。
如果大量使用数组却不知道memoryview,就少了些意思。参见下一节。
内存视图
内置的memoryview类是一个共享内存序列类型,可无需拷贝字节处理数组切片。这是受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:以1x6、2×3或3×2视图处理6字节内存:
>>> from array import array
>>> octets = array('B', range(6)) # 创建一个6字节数组(类型代码'B').
>>> m1 = memoryview(octets) # 通过该数组创建memoryview,然后以列表导出
>>> m1.tolist()
[0, 1, 2, 3, 4, 5]
>>> m2 = m1.cast('B', [2, 3]) # 通过前一个新建一个两行三列的memoryview
>>> m2.tolist()
[[0, 1, 2], [3, 4, 5]]
>>> m3 = m1.cast('B', [3, 2]) # 另一个三行两列的memoryview
>>> m3.tolist()
[[0, 1], [2, 3], [4, 5]]
>>> m2[1,1] = 22 # 在m2的一行一列使用22进行重写
>>> m3[1,1] = 33 # 在m3的一行一列使用33重写
>>> octets # 显示原始数组,证明octets, m1, m2和m3之间共享了内存
array('B', [0, 1, 2, 33, 22, 5])
memoryview的强大能力可用于修改数据。例2-21演示如何在16位整数的数组中修改元素的单个字节。
示例2-21:通过其中一个字节修改16位整数数组元素的值
>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers) # 通过5个16位有符号整数(类型代码'h')的数组创建memoryview
>>> len(memv)
5
>>> memv[0] # memv中的5个元素与原数组相同
-2
>>> memv_oct = memv.cast('B') # 通过将memv中的元素转换为字节(类型代码'B')创建memv_oct
>>> memv_oct.tolist() # 以10字节列表导出memv_oct的元素以供查看
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4 # 将4赋值给字节偏移位置5
>>> numbers
array('h', [-2, -1, 1024, 1, 2]) # 注意数字的变化:2字节无符号整数最高有效字节中的4为1024
注:使用结构体解析二进制记录中有关于通过struct包查看memoryview的示例。
如果在数组中进行高阶数字处理的话,应当使用NumPy库。下面就会进行简短的学习。
NumPy
本书中,我强调了Python标准库已存在的方法,这样大家可以使用到它们。但NumPy太过优秀,值得为它花费些篇幅。
对于高级数组和矩阵运算,NumPy是Python在科学计算应用中成为主流的一大原因。NumPy实现了多维同质数组和矩阵类型,不仅可存储数字,还可存储用记定义记录,并且提供了高效的元素级运算。
SciPy在NumPy基础上编写的库,提供了多种线性代数、微积分和数据统计的科学计算算法。SciPy快速可靠的原因是它使用 Netlib仓库中大量使用的C和Fortran代码。换句话说,SciPy给予科技两个领域的最佳:交互命令行和高级Python API,以及使用C和Fortran优化的业界强大的真大数据运算。
作为一个简短的NumPy演示,例2-22中展现了一些二维数组的基础运算。
示例2-22:numpy.ndarray中的行列基础运算
>>> import numpy as np # 安装后导入NumPy(它不属于Python标准库)。按照惯例,numpy导入为np。
>>> a = np.arange(12) # 创建、查看一个整数0到11的numpy.ndarray
>>> a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>> type(a)
<class 'numpy.ndarray'>
>>> a.shape # 查看数组的维度,这是个一维12个元素的数组。
(12,)
>>> a.shape = 3, 4 # 改变数组的形状,添加一维,然后查看结果
>>> a
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
>>> a[2] # 获取索引为2的行
array([ 8, 9, 10, 11])
>>> a[2, 1] # 获取索引2, 1处的元素
9
>>> a[:, 1] # 获取索引为1的列
array([1, 5, 9])
>>> a.transpose() # 通过转置新建数组(交换行和列)
array([[ 0, 4, 8],
[ 1, 5, 9],
[ 2, 6, 10],
[ 3, 7, 11]])
NumPy还支持加载、保存和对numpy.ndarray的所有元素进行运算的高级运算。
>>> import numpy
>>> floats = numpy.loadtxt('floats-10M-lines.txt') # 通过文本文件加载1千万个浮点数
>>> floats[-3:] # 使用序列切片符号查看最后三个数
array([ 3016362.69195522, 535281.10514262, 4566560.44373946])
>>> floats *= .5 # 对浮点数组中的每个元素乘上.5并再次查看最后三个元素
>>> floats[-3:]
array([ 1508181.34597761, 267640.55257131, 2283280.22186973])
>>> from time import perf_counter as pc # 导入高阶性能计量定时器(Python 3.3中加入)
>>> t0 = pc(); floats /= 3; pc() - t0 # 将每个元素除以3;1千万个浮点数花费时间小于40微秒
0.03690556302899495
>>> numpy.save('floats-10M', floats) # 在.npy二进制文件中保存数组
>>> floats2 = numpy.load('floats-10M.npy', 'r+') # 将数组以内存映射文件加载到另一个数组中;即使它在内存中不能完整拟合,也会让数组切片的处理更高效
>>> floats2 *= 6
>>> floats2[-3:] # 在每个元素乘上6后查看最后三个元素
memmap([ 3016362.69195522, 535281.10514262, 4566560.44373946])
以上只是开胃菜。
NumPy 和 SciPy 都是强大的库,是一些其它强大工具的基础,如Pandas,它实现了可存储非数字数据的高效数组类型,并为 .csv、 .xls、 SQL导出文件和HDF5等格式文件提供了导入导出函数,还有Scikit-learn,它是当前最广泛使用的机器学习工具集。NumPy和SciPy的大部分函数使用C或C++实现,这样可以利用CPU的所有内核,因为它们释放了的Python的GIL(全局解释器锁)。Dask项目支持跨服务器集群的并行NumPy、Pandas和Scikit-Learn运行。这些包可以写好几本书。本书不是介绍它们的。但不讲解NumPy数组对于Python序列又不完整。
在学习了平铺式序列-标准数组和NumPy数组之后,我们现在可以学习对于普通老列表完全不同的替代集:队列。
Deque和其它队列
.append和.pop方法让list可以像栈或队列那样使用(若使用.append和.pop(0)的话,会实现先进先出)。但在列表头部(索引为0的那端)插入、删除元素开销较大,因为整个列表都要在内存中偏移。
collections.deque类是一个线程安全的双端队列,设计用于快速在两端快速插入和删除。如果想像维护一个“最后查看”或此类性质的列表的话它也是好选择,因为deque可带边界,即使用最大长度创建。如果deque设置的边界已满,在添加新元素时它会在另一端删除一个元素。例2-23展示一些对deque执行的一些典型运算。
示例2-23:操作deque
>>> from collections import deque
>>> dq = deque(range(10), maxlen=10) # 可选的maxlen参数设置在deque实例中允许的最大元素数;这会设置一个只读maxlen实例属性
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.rotate(3) # n > 0时旋转在右端取出元素并在左端新增,n < 0时旋转在左端取出元素并在右端新增
>>> 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) # 在deque已满时(len(d) == d.maxlen)添加元素会在另一端删除元素;注意在下一行中删去了0
>>> dq
deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
>>> dq.extend([11, 22, 33]) # 在右端添加3个元素,会在左侧删除 -1, 1 和 2
>>> dq
deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
>>> dq.extendleft([10, 20, 30, 40]) # 注意extendleft(iter)通过在deque左侧添加iter参数的每个连续元素,因此元素最后的位置进行了翻转
>>> dq
deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)
表2-4比较了list和deque的具体方法(此处删去了object中已包含的那些)
可以看到deque实现了list大部分的方法,并添加了一些适合其设计的方法,如popleft和rotate。但存在一个隐藏的开销:在deque的中间删除元素不怎么快。它主要的优化是在两端添加和删除元素。
append和popleft是原子运算,因此在多线程应用中无需使用锁deque将作为先进先出队列使用是安全的。
表2-4:或deque中实现的方法 (为简化省去了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—逆向反复拼接(系列16中讲解) | |
| s.pop() | ● | ● | 删除并返回最后一个元素a_list.pop(p)允许在位置p处删除,但deque并不支持 |
| 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]) | ● | 使用可选关键字参数key和 reverse对元素进行原地排序 |
除deque外,Python标准包还实现了其它的队列:
queue:提供了同步(即线程安全)类SimpleQueue、Queue、LifoQueue和PriorityQueue。它们可用于线程间的安全通信。除SimpleQueue外均可通过在构造函数中提供一个大于0的maxsize参数设置边界。但它们不会像deque那样会删除元素释放空间。而是在队列已满插入新元素时,等待其它线从队列中取出元素来释放空间,这对于限制活跃线程数很有用。multiprocessing:实现其自己的无边界multiprocessing和有界Queue,类似于queue包中那些类,但设计用于跨进程通信。提供了一个专有的multiprocessing.JoinableQueu用于任务管理。asyncio:提供带有受queue和multiprocessing模块中类启发的API的Queue、LifoQueue、PriorityQueue和JoinableQueue,但它适配了异步编程中的任务管理。heapq:与前面三上模块不同,heapq没有实现任何队列类,而是提供了heappush和heappop函数,让我们可使用可变序列作为堆队列或优先级队列。
以上就讲完了list替代类型的综述,并且综合探讨了除str和二进制序列外的序列类型,这两个序列在系列四中会单独讲解。