某些情况下数组不是最好的选择,我们可以使用别的数据类型来替换列表。
2.9.1 数组
如果我们需要一个只包含数字的列表,那么 array.array 比 list 更高效。数组支持所有跟可变序列有关的操作,包括 .pop、.insert 和 .extend。另外,数组还提供从文件读取和存入文件的更快的方法,如 .frombytes 和 .tofile。
Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。
比如 b 类型码 代表的是有符号的字符(signed char),因此 array('b') 创建出的 数组就只能存放一个字节大小的整数,范围从 -128 到 127,这样在序列很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存放除指定类型之外的数据。
# coding: utf-8
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)
fp.close()
print(floats2[-1])
print(floats2 == floats)
从上面的代码我们能得出结论,array.tofile 和 array.fromfile 用起来很简单。把这段代码跑一跑,你还会发现它的速度也很快。一个小试验告诉我,用 array.fromfile 从一个二进制文件里读出 1000 万个 双精度浮点数只需要 0.1 秒,这比从文本文件里读取的速度要快 60 倍,因为后者会使用内置的 float 方法把每一行文字转换成浮点数。 另外,使用 array.tofile 写入到二进制文件,比以每行一个浮点数的方式把所有数字写入到文本文件要快 7 倍。另外,1000 万个这样的数在二进制文件里只占用 80 000 000 个字节(每个浮点数占用 8 个字节, 不需要任何额外空间),如果是文本文件的话,我们需要 181 515 739 个字节。
2.9.2 内存视图(memory view)
memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同 一个数组的不同切片。
内存视图其实是泛化和去数学化的 NumPy 数组。它让你在不需要 复制内容的前提下,在数据结构之间共享内存。其中数据结构可以 是任何形式,比如 PIL 图片、SQLite 数据库和 NumPy 的数组,等 等。这个功能在处理大型数据集合的时候非常重要。
memoryview.cast 的概念跟数组模块类似,能用不同的方式读写同一 块内存数据,而且内容字节不会随意移动。这听上去又跟 C 语言中类型 转换的概念差不多。memoryview.cast会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。
以后再补充。没有理解清楚。
2.9.3 NumPy和SciPy
pass
2.9.4 双向队列和其他形式的队列
利用 .append 和 .pop方法,我们可以把列表当作栈或者队列来用(比如,把 .append 和 .pop(0) 合起来用,就能模拟栈的“先进先出”的特点)。但是删除列表的第一个元素(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。
collections.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])
>>> dq
deque([30, 20, 10, 3, 4, 5, 6, 7, 8, 9], maxlen=10)
collections.deque
1. maxlen 是一个可选参数,代表这个队列可以容纳的元素的数量,而且一旦设定,这个属性就不能修改了。
2. deque.rotate 队列的旋转操作接受一个参数 n,当 n > 0 时,队列的最右边的 n 个元素会被移动到队列的左边。当 n < 0 时,最左边的 n 个元素会被移动到右边。
3. 当试图对一个已满(len(d) == d.maxlen)的队列做尾部添加操作的时候,它头部的元素会被删除掉。注意在下一行里,元素 0 被删除 了。
4. extendleft(iter) 方法会把迭代器里的元素逐个添加到双向队列 的左边,因此迭代器里的元素会逆序出现在队列里。
append 和 popleft 都是原子操作,也就说是 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 方法,让用户可以把可变序列当作堆队列或者优先队列来使用。
2.10 本章小结
Python 序列类型最常见的分类就是可变和不可变序列。但另外一种分类 方式也很有用,那就是把它们分为扁平序列和容器序列。前者的体积更小、速度更快而且用起来更简单,但是它只能保存一些原子性的数据, 比如数字、字符和字节。容器序列则比较灵活,但是当容器序列遇到可变对象时,用户就需要格外小心了,因为这种组合时常会搞出一些“意 外”,特别是带嵌套的数据结构出现时,用户要多费一些心思来保证代 码的正确。
列表推导和生成器表达式则提供了灵活构建和初始化序列的方式,这两 个工具都异常强大。
元组,既可以用作无名称的字段的记录,又可以看作不可变的列表。当作记录时,用到拆包获取字段值。具名元 组也已经不是一个新概念了,但它似乎没有受到应有的重视。就像普通 元组一样,具名元组的实例也很节省空间,但它同时提供了方便地通过 名字来获取元组各个字段信息的方式,另外还有个实用的 ._asdict() 方法来把记录变成 OrderedDict 类型。
Python 里最受欢迎的一个语言特性就是序列切片,而且很多人其实还没 完全了解它的强大之处。比如,用户自定义的序列类型也可以选择支持 NumPy 中的多维切片和省略(...)。另外,对切片赋值是一个修改可变序列的捷径。
重复拼接 seq * n 在正确使用的前提下,能让我们方便地初始化含有 不可变元素的多维列表。增量赋值 += 和 *=会区别对待可变和不可变序列。在遇到不可变序列时,这两个操作会在背后生成新的序列。但如果被赋值的对象是可变的,那么这个序列会就地修改——然而这也取决于序列本身对特殊方法的实现。
序列的 sort 方法和内置的 sorted函数虽然很灵活,但是用起来都不 难。这两个方法都比较灵活,是因为它们都接受一个函数作为可选参数来指定排序算法如何比较大小,这个参数就是 key参数。如果在插入新元素的同时还想保持有序序列 的顺序,那么需要用到 bisect.insort。bisect.bisect 的作用则是快速查找。
除了列表和元组,Python 标准库里还有 array.array。另外,虽然 NumPy 和 SciPy 都不是 Python 标准库的一部分,但稍微学习一下它们, 会让你在处理大规模数值型数据时如有神助。
本章末尾介绍了 collections.deque 这个类型,它具有灵活多用和线程安全的特性。
2.11 杂谈
扁平序列和容器序列概念解释。
有些对象里包含对其他对象的引用;这些对象称为容器。
因此,我特别使用了“容器序列”这个词,因为 Python 里有是容器但 并非序列的类型,比如 dict 和 set。容器序列可以嵌套着使用, 因为容器里的引用可以针对包括自身类型在内的任何类型。
与此相反,扁平序列因为只能包含原子数据类型,比如整数、浮点 数或字符,所以不能嵌套使用。
列表的使用
Python 入门教材往往会强调列表是可以同时容纳不同类型的元素 的,但是实际上这样做并没有什么特别的好处。我们之所以用列表 来存放东西,是期待在稍后使用它的时候,其中的元素有一些通用的特性。
元组则恰恰相反,它经常用来存放不同类型的的元素。这也符合它的本质,元组就是用作存放彼此之间没有关系的数据的记录。
key 参数
list.sort、sorted、max 和 min 函数的 key 参数是一个很棒的 设计。其他语言里的排序函数需要用户提供一个接收两个参数的比 较函数作为参数,像是 Python 2 里的 cmp(a, b)。用 key 参数能 把事情变得简单且高效。说它更简单,是因为只需要提供一个单参 数函数来提取或者计算一个值作为比较大小的标准即可,而 Python 2 的这种设计则需要用户写一个返回值是—1、0 或者 1 的双参数函 数。说它更高效,是因为在每个元素上,key 函数只会被调用一 次。而双参数比较函数则在每一次两两比较的时候都会被调用。