第二章(提炼) 序列构成的数组(四)

一. 用bisect来管理已排序的序列

        bisect 模块包含两个主要函数,bisect insort,两个函数都利用二分查找算法来在有序序列中查找或插入元素。

1.1 用bisect来搜索

演示1  在有序序列中用 bisect 查找某个元素的插入位置

import bisect
import sys

HAYSTACK = [1, 4, 5, 6, 8, 12, 15, 20, 21, 23, 23, 26, 29, 30]
NEEDLES = [0, 1, 2, 5, 8, 10, 22, 23, 29, 30, 31]


def demo(bisect_fn):
    for neddle in reversed(NEEDLES):
        position = bisect_fn(HAYSTACK, neddle)
        offset = position * ' | '
        print(f'{neddle:2d} @ {position:2d}     {offset}{neddle:<2d}')


if __name__ == '__main__':
    if sys.argv[-1] == 'left':
        bisect_fn = bisect.bisect_left
    else:
        bisect_fn = bisect.bisect

    print('DEMO:', bisect_fn.__name__)
    print('haystack ->', ' '.join(f'{n:2d}' for n in HAYSTACK))
    demo(bisect_fn)

运行脚本:python demo1.py

运行脚本时传入参数:python demo1.py left

分析总结:bisect 函数其实是 bisect_right 函数的别名,后者还有个姊妹函数叫 bisect_left。它们的区别在于,bisect_left 返回的插入位置是原序列中跟被插入元素相等的元素的位置,也就是新元素会被放置于它相等的元素的前面,而 bisect_right 返回的则是跟它相等的元素之后的位置。

        bisect 可以用来建立一个用数字作为索引的查询表格,比如说把分数和成绩对应起来。

演示2  根据一个分数,找到它所对应的成绩

1.2 用bisect.insort插入新元素

        排序很耗时,因此在得到一个序列之后,我们最好能够保持它的有序。bisect.insort就是为这个而存在的。insort(seq, item) 把变量 item 插入到序列 seq 中,并能保持 seq 的升序顺序。

演示3  insort 可以保持有序序列的顺序

import bisect
import random

SIZE = 7
l = []
random.seed(0)

if __name__ == '__main__':
    for i in range(SIZE):
        new_item = random.randrange(SIZE * 2)
        bisect.insort(l, new_item)
        print(f'{new_item:2d} -> ', l)

运行结果:

二. 当列表不是首选时

        有时候因为列表实在是太方便了,所以 Python 程序员可能会过度使用它。而如果你只需要处理数字列表的话,数组可能是个更好的选择。比如,要存放 1000 万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是 float 对象,而是数字的机器翻译,也就是字节表述。这一点就跟 C 语言中的数组一样。再比如说,如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。

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

2.1 数组

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

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

演示4  一个浮点型数组的创建、存入文件和从文件读取的过程

        利用一个可迭代对象来建立一个双精度浮点数组(类型码是 'd'),这里我们用的可迭代对象是一个生成器表达式。从上面的代码我们能得出结论,array.tofile 和 array.fromfile 用起来很简单。把这段代码跑一跑,你还会发现它的速度也很快。一个小试验告诉我,用 array.fromfile 从一个二进制文件里读出 1000 万个双精度浮点数只需要 0.1 秒,这比从文本文件里读取的速度要快 60倍,因为后者会使用内置的 float 方法把每一行文字转换成浮点数。

        另外,使用 array.tofile 写入到二进制文件,比以每行一个浮点数的方式把所有数字写入到文本文件要快 7 倍。另外,1000 万个这样的数在二进制文件里只占用 80 000 000 个字节(每个浮点数占用 8 个字节,不需要任何额外空间),如果是文本文件的话,我们需要 181 515 739 个字节。

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

       

2.2 内存视图

        memoryview 是一个内置类,它能让用户在不复制内容的情况下操作同一个数组的不同切片。其中,memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview 对象给你。

演示5  通过改变数组中的一个字节来更新数组里某个元素的值 (这个数组的元素是16位二进制整数)

注:

  1. 负数在计算机内存中以补码的形式存在,对于正数:原码=反码=补码,对于负数:反码——将最高位的符号位以外的数,全部取反;补码——将负数的反码进行加一操作。
  2. 创建memv_oct时 ,把 memv 里的内容转换成 'B' 类型, 表示无符号字符(0~255)。

2.3 Numpy

        NumPy 实现了多维同质数组和矩阵,这些数据结构不但能处理数字,还能存放其他由用户定义的记录。通过 NumPy,用户能对这些数据结构里的元素进行高效的操作。

演示6  对numpy.ndarray的行和列进行基本的操作

    

演示7  Numpy也可以对numpy.ndarray中的元素进行抽象的读取、保存和其它操作

1)构建一个包含100万个浮点数的numpy数组

2)把数组里的每个数都乘以 0.5,然后再看看最后 3 个数(numpy操作100万个数速度非常快)。   

3)把每个元素都除以 3,可以看到处理 100 万个浮点数所需的时间还不足 1.7 毫秒。

4)把数组存入后缀为 .npy 的二进制文件。接着将上面的数据导入到另外一个数组里,这次 load 方法利用了一种叫作内存映射的机制,它让我们在内存不足的情况下仍然可以对数组做切片。

补充:NumPy 和 SciPy 都是异常强大的库,也是其他一些很有用的工具的基石。Pandashttp://pandas.pydata.org)和Blaze(http://blaze.pydata.org)数据分析库就以它们为基础,提供了高效的且能存储非数值类数据的数组类型,和读写常见数据文件格式(例如csv、xlsSQL转储和 HDF5)的功能。

2.4 双向队列和其它形式的队列

        利用 .append .pop 方法,我们可以把列表当作栈或者队列来用(比如,把 .append .pop(0) 合起来用,就能模拟栈的先进先出的特点)。但是删除列表的第一个元素(抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素。

        collections.deque 类(双向队列)是一个线程安全、可以快速从两端添加或者删除元素的数据类型。而且如果想要有一种数据类型来存放“最近用到的几个元素deque 也是一个很好的选择。这是因为在新建一个双向队列的时候,你可以指定这个队列的大小,如果这个队列满员了,还可以从反向端删除过期的元素,然后在尾端添加新的元素。

演示8  使用双向队列

        队列的旋转操作接受一个参数 n,当 n > 0 时,队列的最右边的 n 个元素会被移动到队列的左边。当 n < 0 时,最左边的 n 个元素会被移动到右边。当试图对一个已满(len(d) == d.maxlen)的队列做尾部添加操作的时候,它头部的元素会被删除掉。

        双向队列实现了大部分列表所拥有的方法,也有一些额外的符合自身设计的方法,比如说 popleft rotate。但是为了实现这些方法,双向队列也付出了一些代价,从队列中间删除元素的操作会慢一些,因为它只对在头尾的操作进行了优化。 append 和 popleft 都是原子操作,也就说是 deque 可以在多线程程序中安全地当作先进先出的栈使用,而使用者不需要担心资源锁的问题。

三. 总结

  • Python 序列类型最常见的分类就是可变和不可变序列。但另外一种分类方式也很有用,那就是把它们分为扁平序列和容器序列。前者的体积更小、速度更快而且用起来更简单,但是它只能保存一些原子性的数据, 比如数字、字符和字节。
  • 列表推导和生成器表达式则提供了灵活构建和初始化序列的方式。
  • 元组在 Python 里扮演了两个角色,它既可以用作无名称的字段的记录,又可以看作不可变的列表。具名元组也已经不是一个新概念了,但它似乎没有受到应有的重视。就像普通元组一样,具名元组的实例也很节省空间,但它同时提供了方便地通过名字来获取元组各个字段信息的方式,另外还有个实用的 ._asdict()方法来把记录变成 OrderedDict 类型。
  • Python 里最受欢迎的一个语言特性就是序列切片,用户自定义的序列类型也可以选择支持NumPy 中的多维切片和省略(...)。另外,对切片赋值是一个修改可变序列的捷径。 
  • 重复拼接 seq * n 在正确使用的前提下,能让我们方便地初始化含有不可变元素的多维列表。增量赋值 += *= 会区别对待可变和不可变序列。在遇到不可变序列时,这两个操作会在背后生成新的序列。但如果被赋值的对象是可变的,那么这个序列会就地修改——然而这也取决于序列本身对特殊方法的实现。
  • 序列的 sort 方法和内置的 sorted 函数虽然很灵活,但是用起来都不难。这两个方法都比较灵活,是因为它们都接受一个函数作为可选参数来指定排序算法如何比较大小,这个参数就是 key 参数。key 还可以被用在 min 和 max 函数里。如果在插入新元素的同时还想保持有序序列的顺序,那么需要用到 bisect.insort。bisect.bisect 的作用则是快速查找。
  • collections.deque 具有灵活多用和线程安全的特性。
     
发布了132 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geroge_lmx/article/details/105313133