Python 各显其能的列表

本文最后更新于:2022年7月4日 上午

虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。本文记录 Python 中不同场景应该选择的列表结构。

列表不是首选时

  • 比如要存放 1000 万个浮点数的话,数组(array)的效率要高 得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻 译,也就是字节表述。这一点就跟 C 语言中的数组一样。
  • 如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该 会更快。
  • 如果在代码里,包含操作(比如检查一个元素是否出现 在一个集合中)的频率很高,用 set(集合)会更合适。set 专为检查元素是否存在做过优化。但是它并不是序列,因为 set 是无序的。

数组

如果我们需要一个只包含数字的列表,那么 array.array 比 list 更 高效。数组支持所有跟可变序列有关的操作,包括 .pop.insert.extend。另外,数组还提供从文件读取和存入文件的更快的方法,如 .frombytes.tofile

array 读写

  • 一段读写 array 的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from array import array
from random import random
from time import time
start = time()
floats = array('d', (random() for i in range(10**7)))
fp = open('floats.bin', 'wb')
floats.tofile(fp)
fp.close()
print(f"write time {time() - start}")
start = time()
floats2 = array('d')
fp = open('floats.bin', 'rb')
floats2.fromfile(fp, 10**7)
fp.close()
print(f"read time {time() - start}")
print(floats2 == floats)

-->
write time 3.478039026260376
read time 0.437427282333374
True
  • 用 array.fromfile 从一个二进制文件里读出 1000 万个 双精度浮点数只需要 0.4 秒,这比从文本文件里读取的速度要快 60 倍,因为后者会使用内置的 float 方法把每一行文字转换成浮点数。
  • 另外,使用 array.tofile 写入到二进制文件,比以每行一个浮点数的 方式把所有数字写入到文本文件要快 7 倍。另外,1000 万个这样的数 在二进制文件里只占用 80 000 000 个字节(每个浮点数占用 8 个字节, 不需要任何额外空间),如果是文本文件的话,我们需要 181 515 739 个字节。

内存视图

  • memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同 一个数组的不同切片。
  • 内存视图其实是泛化和去数学化的 NumPy 数组。它让你在不需要 复制内容的前提下,在数据结构之间共享内存。其中数据结构可以 是任何形式,比如 PIL图片、SQLite 数据库和 NumPy 的数组,等 等。这个功能在处理大型数据集合的时候非常重要。
  • memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一 块内存数据,而且内容字节不会随意移动。这听上去又跟 C 语言中类型 转换的概念差不多。memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。

示例代码

通过改变数组中的一个字节来更新数组里某个元素的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import array

numbers = array.array('h', [-2, -1, 0, 1, 2])
memv = memoryview(numbers)
print(len(memv))
memv_oct = memv.cast('B')
print(memv_oct.tolist())
memv_oct[5] = 4
print(numbers)

-->
5
[254, 255, 255, 255, 0, 0, 1, 0, 2, 0]
array('h', [-2, -1, 1024, 1, 2])
  • 将内存中的数据看做单字节无符号数进行修改
  • 在内存上的修改映射到了原始数据上

NumPySciPy

  • 凭借着 NumPy 和 SciPy 提供的高阶数组和矩阵操作,Python 成为科学计 算应用的主流语言。NumPy 实现了多维同质数组(homogeneous array) 和矩阵,这些数据结构不但能处理数字,还能存放其他由用户定义的记 录。通过 NumPy,用户能对这些数据结构里的元素进行高效的操作。

  • SciPy 是基于 NumPy 的另一个库,它提供了很多跟科学计算有关的算 法,专为线性代数、数值积分和统计学而设计。SciPy 的高效和可靠性 归功于其背后的 C 和 Fortran 代码,而这些跟计算有关的部分都源自于 Netlib 库(http://www.netlib.org)。换句话说,SciPy 把基于 C 和 Fortran 的工业级数学计算功能用交互式且高度抽象的 Python 包装起来,让科学 家如鱼得水。

队列

双向队列

  • 利用 .append 和 .pop 方法,我们可以把列表当作栈或者队列来用(比 如,把 .append 和 .pop(0) 合起来用,就能模拟栈的“先进先出”的特 点)。但是删除列表的第一个元素(抑或是在第一个元素之前添加一个 元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。
  • collections.deque 类(双向队列)是一个线程安全、可以快速从两 端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存 放“最近用到的几个元素”,deque 也是一个很好的选择。这是因为在新 建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满 员了,还可以从反向端删除过期的元素,然后在尾端添加新的元素。

其他形式的队列

queue
  • 提供了同步(线程安全)类 Queue、LifoQueue 和 PriorityQueue,不同的线程可以利用这些数据类型来交换信息。这三 个类的构造方法都有一个可选参数 maxsize,它接收正整数作为输入值,用来限定队列的大小。
  • 但是在满员的时候,这些类不会扔掉旧的元 素来腾出位置。相反,如果队列满了,它就会被锁住,直到另外的线程 移除了某个元素而腾出了位置。这一特性让这些类很适合用来控制活跃线程的数量。
multiprocessing
  • 这个包实现了自己的 Queue,它跟 queue.Queue 类似,是设计给 进程间通信用的。同时还有一个专门的 multiprocessing.JoinableQueue 类型,可以让任务管理变得更方 便。
asyncio
  • Python 3.4 新提供的包,里面有 Queue、LifoQueue、PriorityQueue 和 JoinableQueue,这些类受 到 queue 和 multiprocessing 模块的影响,但是为异步编程里的任务管理提供了专门的便利。
heapq
  • 跟上面三个模块不同的是,heapq 没有队列类,而是提供了 heappush 和 heappop 方法,让用户可以把可变序列当作堆队列或者优 先队列来使用。

参考资料


Python 各显其能的列表
https://www.zywvvd.com/notes/coding/python/fluent-python/chapter-2/python-notlist/python-notlist/
作者
Yiwei Zhang
发布于
2022年5月26日
许可协议