流畅的 Python(31):序列构成的数组 2.9&2.9.1

阅读数:11 2019 年 11 月 20 日 17:15

流畅的Python(31):序列构成的数组 2.9&2.9.1

内容简介
本书致力于帮助 Python 开发人员挖掘这门语言及相关程序库的优秀特性,避免重复劳动,同时写出简洁、流畅、易读、易维护,并且具有地道 Python 风格的代码。本书尤其深入探讨了 Python 语言的高级用法,涵盖数据结构、Python 风格的对象、并行与并发,以及元编程等不同的方面。

(当列表不是首选时)

虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。

如果在你的代码里,包含操作(比如检查一个元素是否出现在一个集合中)的频率很高,用 set(集合)会更合适。set 专为检查元素是否存在做过优化。但是它并不是序列,因为 set 是无序的。第 3 章会详细讨论它。

本章余下的内容都是关于在某些情况下可以替换列表的数据类型的,让我们从数组开始。


(数组)

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

Python 数组跟 C 语言数组一样精简。创建数组需要一个类型码,这个类型码用来表示在底层的 C 语言应该存放怎样的数据类型。比如 b 类型码代表的是有符号的字符(signed char),因此 array('b') 创建出的数组就只能存放一个字节大小的整数,范围从 -128 到 127,这样在序列很大的时候,我们能节省很多空间。而且 Python 不会允许你在数组里存放除指定类型之外的数据。

示例 2-20 展示了从创建一个有 1000 万个随机浮点数的数组开始,到如何把这个数组存放到文件里,再到如何从文件读取这个数组。

示例 2-20 一个浮点型数组的创建、存入文件和从文件读取的过程

>>> from array import array  ➊
>>> from random import random
>>> floats = array('d', (random() for i in range(10**7)))  ➋
>>> 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)  ➏
>>> fp.close()
>>> floats2[-1]  ➐
0.07802343889111107
>>> floats2 == floats  ➑
True

❶ 引入 array 类型。

❷ 利用一个可迭代对象来建立一个双精度浮点数组(类型码是 'd'),这里我们用的可迭代对象是一个生成器表达式。

❸ 查看数组的最后一个元素。

❹ 把数组存入一个二进制文件里。

❺ 新建一个双精度浮点空数组。

❻ 把 1000 万个浮点数从二进制文件里读取出来。

❼ 查看新数组的最后一个元素。

❽ 检查两个数组的内容是不是完全一样。

从上面的代码我们能得出结论,array.tofilearray.fromfile 用起来很简单。把这段代码跑一跑,你还会发现它的速度也很快。一个小试验告诉我,用 array.fromfile 从一个二进制文件里读出 1000 万个双精度浮点数只需要 0.1 秒,这比从文本文件里读取的速度要快 60 倍,因为后者会使用内置的 float 方法把每一行文字转换成浮点数。另外,使用 array.tofile 写入到二进制文件,比以每行一个浮点数的方式把所有数字写入到文本文件要快 7 倍。另外,1000 万个这样的数在二进制文件里只占用 80 000 000 个字节(每个浮点数占用 8 个字节,不需要任何额外空间),如果是文本文件的话,我们需要 181 515 739 个字节。

另外一个快速序列化数字类型的方法是使用 pickle https://docs.python.org/3/library/pickle.html )模块。pickle.dump 处理浮点数组的速度几乎跟 array.tofile 一样快。不过前者可以处理几乎所有的内置数字类型,包含复数、嵌套集合,甚至用户自定义的类。前提是这些类没有什么特别复杂的实现。

还有一些特殊的数字数组,用来表示二进制数据,比如光栅图像。里面涉及的 bytesbytearry 类型会在第 4 章提及。

表 2-2 对数组和列表的功能做了一些总结。

表 2-2:列表和数组的属性和方法(不包含过期的数组方法以及那些由对象实现的方法)

列表 数组
s.__add(s2)__ s + s2,拼接
s.__iadd(s2)__ s += s2,就地拼接
s.append(e) 在尾部添加一个元素
s.byteswap 翻转数组内每个元素的字节序列,转换字节序
s.clear() 删除所有元素
s.__contains__(e) s 是否含有 e
s.copy() 对列表浅复制
s.__copy__() copy.copy 的支持
s.count(e) se 出现的次数
s.__deepcopy__() copy.deepcopy 的支持
s.__delitem__(p) 删除位置 p 的元素
s.extend(it) 将可迭代对象 it 里的元素添加到尾部
s.frombytes(b) 将压缩成机器值的字节序列读出来添加到尾部
s.fromfile(f, n) 将二进制文件 f 内含有机器值读出来添加到尾部,最多添加 n
s.fromlist(l) 将列表里的元素添加到尾部,如果其中任何一个元素导致了 TypeError 异常,那么所有的添加都会取消
s.__getitem__(p) s[p],读取位置 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 的值并返回这个值,p 的默认值是最后一个元素的位置
s.remove(e) 删除序列里第一次出现的 e 元素
s.reverse() 就地调转序列中元素的位置
s.__reversed__() 返回一个从尾部开始扫描元素的迭代器
s.__setitem__(p, e) s[p] = e,把位于 p 位置的元素替换成 e
s.sort([key], [revers]) 就地排序序列,可选参数有 keyreverse
s.tobytes() 把所有元素的机器值用 bytes 对象的形式返回
s.tofile(f) 把所有元素以机器值的形式写入一个文件
s.tolist() 把数组转换成列表,列表里的元素类型是数字对象
s.typecode 返回只有一个字符的字符串,代表数组元素在 C 语言中的类型

* 第 13 章会讲反向运算符。

从 Python 3.4 开始,数组类型不再支持诸如 list.sort() 这种就地排序方法。要给数组排序的话,得用 sorted 函数新建一个数组:

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

想要在不打乱次序的情况下为数组添加新的元素,bisect.insort 还是能派上用场(就像 2.8.2 节中所展示的)。

如果你总是跟数组打交道,却没有听过 memoryview,那就太遗憾了。下面就来谈谈 memoryview

图灵地址 https://www.ituring.com.cn/book/1564

评论

发布