流畅的Python(第二版)伴读之 2 序列类型[2/2]

因第2章内容太多,分成了2次发文,本文是其中的第二部分。

在序列中使用 + 和 *

我们希望序列支持 + 和 *运算符。通常,+ 的两个操作数必须是相同的序列类型,两个操作数都不会被修改,而是创建一个相同类型的新序列作为表达式的返回值。

要连接同一序列的多个副本,可将其乘以一个整数。同样,会创建一个新序列:

>>> 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 是一个包含可变的序列时,要小心类似 a * n 这样的表达式,因结果可能会让你大吃一惊。例如,如果尝试将一个列表初始化为 my_list = [[]]* 3 将导致my_list中包含的三个元素在内存中是对同一个列表的引用,这很可能不是你想要的结果。原理如下图

下一节将介绍尝试使用 * 来初始化列表的误区。

建立列表的列表

有时,我们需要用嵌套列表来初始化一个列表--例如,在团队列表中分配学生或在游戏棋盘上表示方格。最好的方法是使用列表推导式,如例 2-14 所示。

例 2-14.长度为 3 的三个列表可以表示井字棋盘

>>> board = [['_'] * 3 for i in range(3)]   # 创建3个list,每个list包含3个元素。
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[1][2] = 'X'   # 修改第1行2列的元素,并查看结果
>>> board
[['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

还有一种做法是经常犯的错误,见下例:

例 2-15 列表的3个元素指向同一list对象,这种列表其实没实际用处

>>> weird_board = [['_'] * 3]* 3  # 外层list的3个元素指向同一个内部的list,看上去没什么问题
>>> weird_board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> weird_board[1][2] = 'O'   # 修改第1行2列的元素,发现所有行都改变了(因为这3行其实指向同一个对象)
>>> weird_board
[['_', '_', 'o'], ['_', '_', 'o'], ['_', '_', ' o ' ]]].

从本质上讲,例2-15的行为与下面的代码类似:

row = ['_'] * 3
board = []
for i in range(3): board.append(row)  # 同一个row在board上添加了三次。

另一方面, 2-14 中的列表理解等同于此代码:

>>> board = []
>>> for i in range(3):
...     row = ['_'] * 3  # 每次迭代都会新建一个row,添加到board
...     board.append(row)
...
>>> board
[['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
>>> board[2][0] = 'X'
>>> board  # 只有第二行改变了,符合预期
[['_', '_', '_'], ['_', '_', '_'], ['X', '_', '_']] 

如果本节中的问题或解决方案还不理解,请看第 6 阐明引用和可变对象的机制和陷阱。

我们已经讨论了在序列中使用+ 和 * 操作符的方法,但还有+= 和 *= 操作符,它们会根据目标序列的可变性产生截然不同的结果。下文将解释它们的工作原理。

序列复合赋值

复合赋值运算符 += 和 *= 根据第一个操作数的不同,表现也截然不同。简单起见,我们先讨论加法复合运算符 (+=),但这些概念也适用于 *= 和其他复合赋值运算符。

使 += 起作用的特殊方法是__iadd__("就地加法")。

然而,如果没有实现__iadd__,Python 就会退回到调用__add__。 请看下面这个简单的表达式:

>>> a += b

如果 a 实现了 __iadd__,则会调用 __iadd__。对于可变序列(如 list、bytearray、array.array),a 将被就地更改(即效果类似于 a.extend(b))。然而,当 a 没有实现 __iadd__时,表达式 a += b 与 a = a + b 的效果相同:表达式 a + b 首先被求值,产生一个新对象,然后绑定到 a。换句话说,变量a指向的对象能否进行就地加法,取决于它有没有实现__iadd__方法。

题外话:这里有点绕,关键是要明白:在Python中,变量相当于一个对象的“标签”。我们对变量赋值,就是拿一个对象,把变量名贴到这个对象上。若a、b都是可变类型list,a=a+b的意思是直接对标签a代表的list对象操作,在它后面追加了b的元素(因为list的__iadd_方法就是这么实现的);若a、b都是不可变的tuple,a=a+b的意思是把a的元素和b的元素都拿出来,拼接到一起,再从新内存区域创建一个tuple对象,然后在这个新tuple对象上贴了a标签。上面两种情形对比,虽然结果都是a的元素融合了原a+b的元素,但是机理是不同的。

一般来说,对于可变序列,可以肯定__iadd__已经实现,具备+=的能力。对于不可变序列,显然不可能实现这一点。

刚才写的关于 += 的内容也适用于 *=,它是通过__imul__实现的。第16章将讨论__iadd_和 __imul__魔术方法。下面用一个可变序列和一个不可变序列演示*=的使用:

>>> lst = [1, 2, 3]
>>>	id(l) 
4311953800  # lst的初始id(可以认为是对象存储的内存地址,当然,这只是逻辑地址)
>>> lst *= 2
>>> lst
[1, 2, 3, 1, 2, 3]
>>>	id(l) 
4311953800  #复合赋值后,lst还是原来的那个对象,只是元素增加了
>>> t = (1, 2, 3)
>>>	id(t) 
4312681568   #t的初始id
>>> t *= 2
>>>	id(t) 
4301348296  #复合赋值后,产生了新的tuple对象,并赋给t

重复连接不可变序列的效率很低,因为解释器必须复制整个对象,创建一个新序列,而不是简单地添加新项目(但str是个例外,str是不可变类型,但单独做了优化,拼接时不必然重复创建对象)。

我们已经了解了 += 的常见用例。下一节将展示一个有趣又怪诞的案例,突出说明"不可变 "在元组应用中的真正含义。

+= 的迷惑

在不实操的情况下试着回答: 2-16 中两个表达式的结果是什么?

例 2-16 元组赋值测试

>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]

请选择最佳答案:

A. t  变成(1,2,[30,40,50,60])。

B. 抛出TypeError错误信息: 'tuple' object does not support item assignment。

C. 都不是。

D. A 和 B 都对。

也许你很确定答案是 B,但实际上是 D,即 “A 和 B 都对”!2-17 是Python 3.9 控制台的实际输出。

有读者指出,示例中的操作可以用 t[2].extend([50,60]) 完成,不会出错。我意识到了这一点,但我的意图是展示 += 运算符在这种情况下的奇怪行为。

例 2-17 意外结果:元素t[2] 被更改并引发异常

>>> 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])

Online Python Tutor 是一个非常棒的在线工具,它可以可视化展示Python 的工作原理。 2-5 是两张截图,显示了 2-17 中元组 t 的初始和最终状态。

图 2-5 元组赋值测试的初始和最终状态(Online Python Tutor 生成的图)

查看表达式 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   #将 s[a] 的值放到 TOS(堆栈顶部)
         8        LOAD_NAME            2 (b)
         11       INPLACE_ADD     #如果 TOS 指向可变对象(例 2-17 中是list),则执行 TOS+= b。
         12       ROT_THREE
         13       STORE_SUBSCR    # 赋值s[a]=TOS。如果 s 不可变(例 2-17 中t是元组),则此操作失败
         14       LOAD_CONST           0 (None)
         17       RETURN_VALUE

这个例子设计的很怪诞,在使用 Python 的 20 年中,我(《流畅的Python》作者)从未见过有人被这个坑过。

总结三条教训:

  1. 不要将可变的元素放入元组中。
  2. 复合赋值不是原子操作——我们刚刚看到它在进行一部分步骤后抛出异常。
  3. 检查 Python 字节码并不难,而且有助于了解解释器层面下发生了什么。

在了解了使用 + 和 * 进行连接的精髓之处后,我们可以开始序列的另一个基本操作:排序。

list.sort方法 VS 内置sorted函数

list.sort 方法对列表进行就地排序,即不产生新list对象。它返回 None 来提醒我们它改变了原对象而不是创建了一个新的列表。这是 Python API 的一个重要约定: 就地改变对象的函数或方法应该返回 None,以便让调用程序清楚地知道原对象被改变了,而没有创建新的对象。例如,类似的行为可以在dom.shuffle(s)  函数中看到,该函数就地对可变序列 s 打乱顺序,并返回 None。

用返回 None 来表示就地更改的惯例有一个缺点:我们不能级联调用这些方法。相反,返回新对象的方法(如str的所有方法)可以级联调用。有关此主题的更多描述,请参阅维基百科的 "Fluent interface "条目

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

list.sort 和 sorted 都有两个可选的关键字参数:

  • reverse:如果为 True,则按降序排序。默认值为 "假"。
  • key:只有一个参数的函数,对每个元素调用此函数,生成排序权重。例如,在对字符串列表排序时, key=str.lower 可用于执行大小写不敏感排序,key=len 将按字符长度对字符串排序。默认情况下使用恒等函数(恒等函数是返回相同值的函数:lambda x: x。也就是对元素本身的值进行比较)。

可选参数key也可以使用内置函数 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']     #按长度排序。由于排序算法是稳定的,所以长度均为 5 的 "葡萄 "和 "苹果 "都按照原列表的顺序排列
>>> sorted(fruits, key=len, reverse=True)
['raspberry', 'banana', 'grape', 'apple']     #按长度从大到小排序。由于排序是稳定的,所以 "葡萄 "再次出现在"苹果 "之前
>>> fruits
['grape', 'raspberry', 'apple', 'banana']     #原始的fruits排序没有改变
>>> fruits.sort()                                       #这将对列表进行就地排序,并返回None(控制台不会输出)
>>> fruits
['apple', 'banana', 'grape', 'raspberry']     #原始的fruits被改变

警告:默认情况下,Python 按字符编码的词典顺序排序字符串。这意味着ASCII 大写字母会排在小写字母之前,而非 ASCII 字符则不可能以合理的方式排序。148 页上的 "排序 Unicode 文本 "介绍了按照期望方式排序的正确方法。

一旦序列排序完毕,就可以非常高效地进行搜索。Python 标准库的 bisect 模块已经提供了二分搜索算法。该模块还包括 bisect.insort 函数,用它在可变序列中插入元素并仍然保持排序。flu- entpython.com 配套网站上的 "用 Bisect 管理有序序列 "一文中有关于 bisect 模块的图解介绍。

到目前为止,我们在本章中看到的大部分内容都适用于一般的序列,而不仅仅是list 或 tuples。Python 程序员有时会过度使用list类型,因为它太方便了。例如,如果处理大量数值型list,应该考虑使用array来代替它。下面的内容专门讨论list和tuple的替代品。

当list不是最优解

list类型灵活易用,但实际情况可能还有更好的选择。例如,当处理数百万浮点数值时,array(数组)可以节省大量内存。另一方面,如果需要不断从list的两端添加和删除元素,那么最好了解一下deque(双端队列)是一种更高效的FIFO数据结构(First in, first out先进先出)。

如果您的代码经常检查某个元素是否存在于某个集合中(例如, my_collection  中的  item),请考虑my_collection  使用set,尤其是当它包含大量元素时。set经过优化,可以快速检查成员关系。set也是可迭代的,但它们不是序列,因为set内元素是无序的,无法进行排序。我们将在3 介绍set。

在本章的其余部分,我们将从array开始,讨论可变序列中可以替代list的诸多情形。

数组Array

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

Python 中的array和 C 数组一样精简。如图2-1所示,一个float类型的array不保存完整的float实例,而只保存原始值-- 类似于 C 语言中的double 数组。创建array时,需要提供一个类型符(typecode),即确定对应底层 C 语言类型的字母。例如,b 是 C 语言中signed char的类型符,是一个范围在 -128 到 127  之间的整数。如果创建一个 array('b'), 那么每个元素都将存储在一个字节中,并解释为一个整数。对于大的数字序列, 这可以节省大量内存。而且 Python 允许输入任何与数组类型不匹配的数字。

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

例 2-19.创建、保存和加载大型浮点数组

>>> from array import array       # 导入array类型
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7)))   #使用生成器创建双精度浮点数组(类型码为 "d")
>>> floats[-1]    #查看array的最后一个元素
0.07802343889111107
>>> fp = open('floats.bin', 'wb')
>>> floats.tofile(fp)   # 保存为二进制文件
>>> fp.close()
>>> floats2 = array('d')  # 创建一个空的double类型数组
>>> fp = open('floats.bin', 'rb')
>>> floats2.fromfile(fp, 10**7)  # 从二进制文件中读取 1000 万个数值,注意不是文件的1000万个字节
>>> fp.close()
>>> floats2[-1]    #查看array的最后一个元素
0.07802343889111107
>>> floats2 == floats    # 验证数组内容是否正确
True

正如你所看到的,array.tofile 和 array.fromfile 很容易使用。如果运行这个示例,会发现它们的速度也非常快。速度实验表明,用array.tofile 创建了1 千万个double类型的二进制文件后,用array.fromfile 加载只需 0.1 秒。这比从文本文件中读取数字的速度快了近 60 倍,而且从文本文件中读取数字还需要使用内置的float函数进行值类型转换。使用 array.tofile 保存数据比在文本文件中每行写入一个浮点快约 7 倍。此外,包含 1000 万个double数值的二进制文件的大小为 8千万字节(每个双字节 8 字节,零开销),而文本文件中相同数据的大小为 181,515,739 (约1.8亿)字节。

对于特殊的数值型(如表示图像二进制数据)的数组,Python 提供了4 的bytes和bytearray类型。

表 2-3 是本节array内容的收尾部分,其中比较了list和array.array功能。

表 2-3 list和array中的方法和属性(为简洁起见,省略了继承自object的方法)

方法

list

array

说明

s.__add__(s2)

s + s2—合并

s.__iadd__(s2)

s += s2—就地合并

s.append(e)

在末尾追加元素

s.byteswap()

对每个元素的字节序进行反转

s.clear()

清空所有元素

s.__contains__(e)

s是否包含s

s.copy()

浅拷贝

s.__copy__()

用于支持copy.copy

s.count(e)

计算e在s内出现的次数

s.__deepcopy__()

用于支持copy.deepcopy,并做了优化

s.__delitem__(p)

删除位置在p的元素

s.extend(it)

将可迭代对象it的元素追加到s末尾

s.frombytes(b)

把字节序列解析成二进制数字值,并追加到s末尾

s.fromfile(f, n)

从文件f读取解析成二进制数字值,并追加n个元素到s末尾

s.fromlist(l)

从list中追加元素到s末尾,如果报TypeError则不追加任何元素

s.__getitem__(p)

s[p]—通过下标访问元素或切片

s.index(e)

返回元素e在s中第一次出现的位置

s.insert(p,  e)

在位置p的元素前插入e

s.itemsize

每个数组元素占用的字符数

s.__iter__()

获取迭代器

s.__len__()

len(s)—元素数量

s.__mul__(n)

s * n—重复n次合并

s.__imul__(n)

s  *=  n—重复n次合并并就地赋值

s.__rmul__(n)

n  * s—逆向运算的重复合并

s.pop([p])

删除并返回p位置的元素,不传入p时删除并返回最末尾元素

s.remove(e)

删除第一个等于e的元素

s.reverse()

反转元素顺序

s.__reversed__()

获取反转顺序的迭代器

s.__setitem__(p, e)

s[p] = e—将p位置的元素或切片替换为e

s.sort([key],[reverse])  

使用可选的参数key、reverse进行排序

s.tobytes()

以bytes对象形式返回所有元素的二进制值

s.tofile(f)

将所有元素的二进制值写入到二进制文件f

s.tolist()

将元素的数字型值转为list

s.typecode

返回C语言类型的标识字符

从 Python 3.10 开始,array类型没有像 list.sort() 一样的就地排序方法。如果需要对array排序,请使用内置的 sorted 函数:

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

要在插入元素后保持已排序数组的排序,可使用bisect.insort 函数。

如果你经常使用array,那就值得去了解了解 memoryview。请参见下一节。

memoryview

内置的 memoryview 类是一种共享内存序列类型,可以在不复制字节的情况下处理array片段。它的灵感来源于 NumPy 库(我们将在下一节 "NumPy "部分讨论该库)。NumPy  的主要作者(Travis Oliphant是这样回答"何时应该使用内存视图 "这个问题的:

Python 中memoryview本质上和NumPy 数组结构基本一致。它允许你在数据结构(如 PIL 图像、SQLite 数据库、NumPy 数组等)之间共享内存,而无需先复制。这对于大型数据集来说非常重要。

memoryview.cast 方法可以不必移动内存中的基数据,就能用不同的长度单位读取或写入一段字节。memoryview.cast 返回另一个memoryview 对象,他们始终共享相同的内存。

2-20 展示了如何在同一个 6 字节的数组上创建不同视图,将其组成 2×3 矩阵或 3×2 矩阵进行操作。

例 2-20.以 1×6、2×3 和 3×2 视图处理 6 字节内存

>>> from array import array
>>> octets = array('B', range(6))  # 构建6个字节的array(类型标识符是'B')
62 | Chapter 2: An Array of Sequences
>>> m1 = memoryview(octets)  # 在数组基础上创建memoryview,并输出为list查看
>>> m1.tolist()
[0, 1, 2, 3, 4, 5]
>>> m2 = m1.cast('B', [2, 3])  # 在m1的基础上创建新memoryview,变形为2行、3列
>>> m2.tolist()
[[0, 1, 2], [3, 4, 5]]
>>> m3 = m1.cast('B', [3, 2])  # 又一个memory view,变形为3行2列
>>> m3.tolist()
[[0, 1], [2, 3], [4, 5]]
>>> m2[1,1] = 22  # 在m2中将1行1列(行列数从0开始)的数据修改为22
>>> m3[1,1] = 33  # 在m3中将1行1列(行列数从0开始)的数据修改为33
>>> octets  # 显示原始数组,证明内存由octets、 m1、m2 和 m3共享。
array('B', [0, 1, 2, 33, 22, 5]) 

内存视图的强大功能还在于修改数据。 2-21 展示了如何更改 16 位整数数组中某一元素的一个字节。

例 2-21.操作array中16 位整数元素的一个字节

>>> numbers = array.array('h', [-2, -1, 0, 1, 2])
>>> memv = memoryview(numbers)  # 基于5个16位整型元素的arry创建memoryview(类型标识符'h')
>>> len(memv)
5
>>> memv[0]  # memv持有arry中的全部5个元素
-2
>>> memv_oct = memv.cast('B')  # 从memv创建memv_oct,类型标识符设置为'B'
>>> memv_oct.tolist()  # 从memv_oct导出的list含有10个字节元素。
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
>>> memv_oct[5] = 4  # 为索引5的位置赋值4
>>> numbers

array('h', [-2, -1, 1024, 1, 2])  # 注意numbers的变化:'00000100 00000000'(高字节4,低字节0)的值是1024

访问fluentpython.com的“Parsing binary records with struct”可以看到使用struct包分析memoryview的例子

如果想搞一些array的高级玩法,则应使用NumPy 库。下面就来简单了解一下它。

NumPy

本书一直关注的是Python 标准库中的内容,但NumPy 实在是太棒了,所以有必要纳入进来。

在高级数组和矩阵操作方面,NumPy 的存在使 Python 成为科学计算应用的主流。NumPy 实现了多维、同构数组(,数组中所有的元素都是同种类型的)和矩阵类型,不仅能保存数字,还能保存用户定义的记录,并提供高效的元素操作。

SciPy 是在 NumPy 基础上编写的一个库,提供线性代数、数值微积分和统计学中的许多科学算法。SciPy 利用了 Netlib 代码仓库中广泛使用的 C 和Fortran 代码库,因此快速可靠。换句话说,SciPy  为科学家提供了两全其美的解决方案:交互式界面和高级 Python 应用程序接口,以及用 C 和 Fortran 优化的工业级数值计算函数。

2-22 展示了一些二维数组的基本操作。

例 2-22.对 numpy.ndarray 中的行和列进行基本操作

>>> import numpy as np   # numpy需要单独按照,然后导入numpy,通常会命名为np
>>> a = np.arange(12)  # 创建和查看一个numpy.ndarray对象,它包含数值0到12
>>> 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')  # 从文本文件中加载 1000 万个浮点数。
>>> floats[-3:]   # 使用切片检查最后三个数字
array([ 3016362.69195522, 535281.10514262, 4566560.44373946])
>>> floats *= .5   # 将浮点数组中的每个元素乘以0.5,然后再次检查最后3个元素。
>>> 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;1000 万个浮点数的耗时小于 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 dumps、HDF5 等)提供导入/导出函数,再例如 scikit-learn(目前使用最广泛的机器学习工具集)。大多数 NumPy 和 SciPy 函数都是用 C 或 C++ 实现的 ,由于它们运行摆脱了 Python 的 GIL(全局解释器锁),因此可以充分利用所有 CPU 内核。Dask 项目支持在机器集群中并行处理 NumPy、Pandas 和 scikit-learn。这些软件包都有单独的书籍去介绍,但还是要介绍NumPy 数组,不然对 Python 序列的介绍将是不完整的。

在了解了扁平型序列——标准array和NumPy 数组之后,再介绍一个完全不同的list替代品:队列。

双端队列deque和其他队列

list的.append 和 .pop 方法可使其用作堆栈或队列(如果使用 .append 和 .pop(0), 则体现了先进先出——FIFO 的行为)。但是,从list的头部(索引为0的一端)插入和移除元素的代价很高,那样整个列表必须在内存中整体前后移动。

collections.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)  # rotate的参数 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])  # 在右边添加三个元素会推掉最左边的-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) 的工作原理是将 iter 参数的每个元素
                                #连续地(而不是整体一次性)追加到deque 的左边,因此项的最终位置是相反的
>>> 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 list 或 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)

s是否包含s

s.copy()

list的浅拷贝

s.__copy__()

用于支持copy.copy (浅拷贝)

s.count(e)

计算e在s内出现的次数

s.__delitem__(p)

删除位置在p的元素

s.extend(it)

将可迭代对象it的元素追加到s末尾/右端

s.extendleft(i)

将可迭代对象it的元素追加到s头部/左端

s.__getitem__(p)

s[p]—通过下标或切片访问元素

s.index(e)

返回元素e在s中第一次出现的位置

s.insert(p,  e)

在位置p的元素前插入e

s.__iter__()

获取迭代器

s.__len__()

len(s)—元素数量

s.__mul__(n)

s * n—序列重复n次合并

s.__imul__(n)

s *= n—就地重复合并

s.__rmul__(n)

n  * s—逆向运算的重复合并(见第16章)

s.pop()

删除并返回最末尾元素

s.popleft()

删除并返回第一个元素

s.remove(e)

删除第一个等于e的元素

s.reverse()

就地反转元素顺序

s.__reversed__()

获取反转顺序的迭代器

s.rotate(n)

移动n个元素到队列另一端

s.__setitem__(p, e)

s[p] = e—将p位置的元素修改为e(可以修改序列,见“为切片赋值”)

s.sort([key],[reverse])

使用可选的参数key、reverse进行排序

除了 deque 之外, Python 标准库软还有其他类型的队列程序包(注意下面这些标题是包,不是类):

  • queue

它提供了同步(即线程安全)类 SimpleQueue 、 Queue 、 LifoQueue 和PriorityQueue。这些类可用于线程间的安全通信。除 SimpleQueue 外,其他所有类都可以通过向构造函数提供一个大于 0 的参数限制队列长度。不过,它们不像 deque 那样丢弃元素来腾出空间。而是当队列已满时,会阻塞新元素的插入——也就是说,它会一直等到其他线程从队列中取出元素来腾出空间,这对于控制同时运行线程的数量非常有用。

  • multiprocessing

实现了自己的不限长SimpleQueue类和限制长度的Queue,与queqe包中的同名类型非常相似,但设计用于进程间通信。还为任务管理提供了专门的multiprocessing.JoinableQueue。

  • asyncio

提供了Queue、LifoQueue、PriorityQueue 和 JoinableQueue,他们的API于queue和multiprocessing里的类相似,但适用于异步编程中的任务管理。

  • heapq

与前三个模块不同的是,heapq 没有实现队列类,而是提供了 heappush 和heappop 等函数,让你可以将一个可变序列用作堆队列或优先级队列。

至此,我们结束了对替代list类型的方案介绍,以及对序列类型的总体探讨——除了特殊的str 和二进制序列,这两种类型有单独的章节(第 4 )。

本章总结

掌握标准库序列类型是编写简洁、有效和规范化 Python 代码的先决条件。

Python  序列通常被分为可变序列和不可变序列。另一种分类方法是:扁平序列和容器序列。前者更紧凑、更快、更易用,但不能嵌套,仅限于存储原子数据,如数字、字符和字节。容器序列更加灵活,但在存储可变对象时可能会出现不可预料的结果,因此 需要小心将其与嵌套数据结构正确地结合起来使用。

遗憾的是,Python 没有万无一失的不可变容器序列类型:即使是 "不可变 "的元组, 当它们包含可变项(如列表或用户定义的对象)时,其值也可能被改变。

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

Python 中的元组有两种作用:作为无名字段的记录和作为不可变列表。在将元组用作不可变列表时,请记住,只有当元组中的所有项都不可变时,元组的值才能保证是固定的。对一个元组调用 hash(t) 是断言其值是固定值的快速方法。如果 t 包含可变项,则会引发 TypeError。

当元组用作记录时,元组解包是提取元组字段的最安全、最易读的方法。除了元组,* 还能在许多上下文中用于 list 和 iterable(可迭代类型),它的一些用例出现在 Python 3.5 的 PEP 448 - Additional Unpacking Generalizations 中。Python 3.10 引入了带有match/case 的模式匹配,支持更强大的解包,即解构赋值。

序列切片是 Python 最受欢迎的语法特性,它甚至比许多人意识到的还要强大。自定义序列类也可以支持NumPy 中使用的多维切片和省略号(...)符号。为切片赋值是编辑可变序列的一种非常有用的方式。

重复合并(如 seq * n )非常方便,而且只要小心谨慎,还可用于初始化包含不可变项的列表。对于可变序列和不可变序列,使用 += 和 *= 的增量赋值表现不同。对于不可变序列,这些操作符必须建立新的序列。但如果序列是可变的,则通常会就地更改——这只是多数情况但不全部如此,这取决于序列的实现方式。

sort方法和sorted内置函数使用方便灵活,这要归功于可选的 key 参数:排序函数。顺便说一下,key 也可以与 min 和 max 内置函数一起使用。

除了 list 和 tuples 之外,Python 标准库还提供了 array.array。虽然 NumPy 和SciPy 并不是标准库的一部分,但如果您要对大型数据集进行任何类型的数值处理,哪怕只是学习这些库中的一小部分,也会让您受益匪浅。

最后是多功能、线程安全的 collections.deque,在2-4 中比较了它与list 的 API,并提到了标准库中其他队列的实现。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值