当Python列表不适用时,该如何选择?

在编写Python代码时,我们有时过度使用列表,因为列表实在是太方便了。list类型简单灵活,不过有时针对具体的需求,或许还有更好的选择。

例如处理大型数值列表时,应考虑使用数组,数组处理数百万个浮点值可以节省大量内存。如果需要经常在列表的两端添加和删除项,使用deque(double-ended queue,双端队列)更合适,这是一种高效的FIFO(first in first out 即先进先出,这是队列默认的行为)数据结构。如果你在代码中经常检查容器中是否存在某一项,例如(item in collection)应考虑使用set类型存储collection,python对set成员检测做了优化,速度更快。set也是可迭代对象,但不是序列,因为set中的项是无序的。

数组

如果一个列表只包含数值,那么使用array.array会更加高效。数组支持所有可变序列操作(包括pop,insert和extend),此外还有快速加载项和保存项的方法,例如frombytes和tofile。

python数组像C语言一样精简。创建array对象时要提供类型代码,它是一个字母,用来确定底层使用什么C类型存储数组中各项。另外Python不允许向数组中添加与指定类型不同的值。

下面示例创建一个包含1000万个随机浮点数的数组,把这些浮点数存入文件,再从文件中读取出来。

from array import array  # 导入
from random import random

# 从一个可迭代对象(这里使用的是一个生成器表达式)中创建一个双精度浮点数数组(类型代码为‘d’)
floats = array('d', (random() for i in range(10 ** 7)))
print(floats[-1])  # 输出数组中最好一个数

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

print(floats2[-1] == floats[-1])  # 确认两个数组的内容是不是一致

经试验,使用array.fromfile从array.tofile创建的二进制文件中加载1000万个双精度浮点数比从文本文件中读取快了近60倍,而且无需使用内置函数float解析每一行,同时内存的消耗也少很多。

memoryview

经常使用数组的人如果不知道memoryview,可谓抱憾终身。内置的memoryview类是一种共享内存的序列类型,可在不复制字节的情况下处理数组的切片。memeryview类的灵感来自Numpy。

memoryview.cast方法使用的表示法与array模块类似,作用是改变读写多字节单元的方式而无需移动位。该方法返回的另一个对象,而且始终共享内存。

下面示例展示了如何将同一个6字节数组处理为不同的视图,先是一个2x3矩阵,或是一个3x2矩阵。

from array import array

octets = array('B', range(6))  # 创建一个6字节的数组,类型代码为'B'
m1 = memoryview(octets)  # 根据这个数组创建一个memoryview对象,然后导出一个列表
m1_list = m1.tolist()
print(m1_list)  # [0, 1, 2, 3, 4, 5]
m2 = m1.cast('B', [2, 3])  # 根据前一个memoryview对象构建一个新的memoryview对象,不过是2行3列
m2_list = m2.tolist()
print(m2_list)  # [[0, 1, 2], [3, 4, 5]]
m3 = m1.cast('B', [3, 2])  # 在构建一个memoryview对象,这一次是3行2列
m3_list = m3.tolist()
print(m3_list)  # [[0, 1], [2, 3], [4, 5]]
m2[1, 1] = 22
m3[1, 1] = 33
print(octets)  # array('B', [0, 1, 2, 33, 22, 5])

双端队列和其他队列

借助append和pop方法,列表可以当做栈或队列使用(append和pop(0)实现的是先进先出行为)。但是,在列表头部(索引位为0)插入和删除项有一定的开销。因为整个列表必须在内存中移动。

collections.deque类实现一种线性安全的双端队列,旨在快速在两端插入和删除项。如果需要保留"最后几项",或实现类似行为,则双端队列是最好的选择。因为双端队列可以有界,即长度固定。有界的deque对象填满以后,从一端加新项,将从另一端丢弃一项。

from collections import deque

# 创建一个deque实例,可选参数maxlen设定队列中最多运行存放多少项。maxlen也是deque实例的一个可读熟悉
dq = deque(range(10), maxlen=10)
print(dq)  # deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
dq.rotate(3)  # 轮转,当里面的参数n>0时,从右端取几项放到左端;当n<0,从左端取几项放到右端
print(dq)  # deque([7, 8, 9, 0, 1, 2, 3, 4, 5, 6], maxlen=10)
dq.rotate(-4)
print(dq)  # deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], maxlen=10)
dq.appendleft(-1)  # 向已满的队列中追加几项,则另外一段要丢弃几项
print(dq)  # deque([-1, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
dq.extend([11, 22, 33])  # 向右端添加3项,把左端前三项挤出队列
print(dq)  # deque([3, 4, 5, 6, 7, 8, 9, 11, 22, 33], maxlen=10)
dq.extendleft([10, 20, 30, 40])  # extendleft(iter)依次把iter参数中的各项追加到队列的左端,因此项之间的位置顺序得到保留
print(dq)  # deque([40, 30, 20, 10, 3, 4, 5, 6, 7, 8], maxlen=10)

注意:deque实现了多数的list方法,另外还增加了一些专用方法,如poplift和rotate。不过队列也有局限性:从deque对象中部删除项的速度不快。双端队列优化的是在两端增减项的操作。append和poplift是原子操作,因此可以发现的在多线程应用中把deque作为先进先出的队列使用,无需加锁。

另外处理deque外,Python标准库中的其他包还实现了以下队列:

  • queue:提供几个同步(即线程安全)队列类:SimpleQueue、Queue、LifoQueue 和 PriorityQueue。这些类可在线程之间安全通信。除 SimpleQueue 之外,其他几个类都可以有界——为构造 函数提供 maxsize 参数,设为大于0 的值。但是,它们不像 deque 那样为了腾出空间而把项丢弃,而是在队列填满后阻塞插⼊新项,等待其他线程从队列中取出⼀项。利用这种行为可以限制活动线程的数量。
  • multiprocessing:单独实现了⽆界的 SimpleQueue 和有界的 Queue。这与 queue 包中的队列类⾮常相似,只不过专门针对进程间通信。它还为 任务管理提供了专⽤的 multiprocessing.JoinableQueue。
  • asyncio:提供了 Queue、LifoQueue、PriorityQueue 和 JoinableQueue,API 源自 queue 和 multiprocessing 模块中 的类,不过为管理异步编程任务而做了修改。
  • heapq:与前三个模块相比,heapq 没有实现任何队列类,⽽是提供了 heappush 和 heappop 等函数,可把可变序列当作堆队列或优先级 队列使⽤。

最后我想提供一个序列的冷知识:为什么切片和区间要排除最后一项

  • 切片和区间排除最后一项是一种 Python 风格约定,这与 Python、C 和 很多其他语⾔中从零开始的索引相匹配。排除最后⼀项可以带来以下好处。
  • 在仅指定停⽌位置时,容易判断切片或区间的⻓度。例如, range(3) 和 my_list[:3] 都只产⽣ 3 项。
  • 同时指定起始和停⽌位置时,容易计算切片或区间的⻓度,做个减法即可:stop - start。
  • 方便在索引 x 处把⼀个序列拆分成两部分而不产生重叠,直接使用my_list[:x] 和 my_list[x:] 即可。

 

 

 

  • 22
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值