流畅的python:序列构成的数组-Part2

第二章 序列构成的数组-Part2

1、序列的增量赋值

增量赋值运算符为+=和*=,笔者以前只知道a+=b等价于a=a+b,剩下的并没有深入的思考,看了这本书以后我才知道原来其表现形式也有区分:而其表现取决于它们的第一个操作对象。

下面我们以+=为例,说明增量赋值后续的原理与表现形式:

如果你仔细看过第一章,应该知道+=运算符本质上调用的是__iadd__特殊方法,但是如果一个类没有实现这个方法的话,Python会退一步调用__add__ 。对于可变序列,a+=b等价于a.extend(b),a的内存地址不发生改变;
对于不可变序列,例如元组,因为a不可变,其基本过程是先计算a+b,然后又新创建一个地址,把值再赋给a,也就是说前后a的内存地址是不一样的。

# 可变元素地址不变
a = [1, 2]
print(id(a)) # 2
a *= 2
print(id(a)) # 2

# 不可变元素地址发生改变
b = (1, 2)
print(id(b)) # 3
b *= 2
print(id(b)) # 3

所以对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。str是一个例外,因为对字符串做+=实在是太普遍了,所以CPython对它做了优化。为str初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。

2、当列表不是最优选择时

虽然列表既灵活又简单,但面对各类需求时,我们可能会有更好的选择。比如,要存放1000万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是float对象,而是数字的机器翻译,也就是字节表述。这一点就跟C语言中的数组一样。如果在你的代码里,包含操作(比如检查一个元素是否出现在一个集合中)的频率很高,用set(集合)会更合适。set专为检查元素是否存在做过优化。但是它并不是序列,因为set是无序的。

  1. 数组

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

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

    常见的类型码如下表所示:

    Type code C Type Python Type Minimum size in bytes
    ‘b’ signed char int 1
    ‘B’ unsigned char int 1
    ‘u’ Py_UNICODE Unicode character 2
    ‘h’ signed short int 2
    ‘H’ unsigned short int 2
    ‘i’ signed int int 2
    ‘I’ unsigned int int 2
    ‘l’ signed long int 4
    ‘L’ unsigned long int 4
    ‘q’ signed long long int int 8
    ‘Q’ unsigned long long int int 8
    ‘f’ float float 4
    ‘d’ double float 8

    利用array也会快速将数据保存为文件以及从存储中读取文件,如下面代码所示:

    from array import array
    from random import random
    floats = array('d', (random()for i in range(10**7)))
    
    # 写入二进制文件,在三石电脑上运行花费65ms
    fp = open('floats.bin', 'wb')
    floats.tofile(fp)
    fp.close()
    
    # 读取二进制文件,在三石电脑上运行花费109ms
    floats2 = array('d')
    fp = open('floats.bin', 'rb')
    floats2.fromfile(fp, 10**7)
    fp.close()
    

    在实际运行过程中,速度也很快,用array.fromfile从一个二进制文件里读出1000万个双精度浮点数只需要0.1秒,这比从文本文件里读取的速度要快60倍,因为后者会使用内置的float方法把每一行文字转换成浮点数。另外,使用array.tofile写入到二进制文件,比以每行一个浮点数的方式把所有数字写入到文本文件要快7倍。另外,1000万个这样的数在二进制文件里只占用80000000个字节(每个浮点数占用8个字节,不需要任何额外空间),如果是文本文件的话,我们需要181515739个字节。所以在数据保存时引优先使用二进制文件,节省空间并加快读取速度,不过随之而来的是可读性变差。

    另外一个快速序列化数字类型的方法是使用pickle模块。pickle.dump处理浮点数组的速度几乎跟array.tofile一样快,我们在这里不再详述,或许大家需要记住的只有:pickle.dump和pickle.load两个方法:

    path = 'test'
    f = open(path, 'wb')
    data = {'a':123, 'b':'ads', 'c':[[1,2],[3,4]]}
    pickle.dump(data, f)
    f.close()
    
    f1 = open(path, 'rb')
    data1 = pickle.load(f1)
    print(data1)
    
  2. NumPy和SciPy

    凭借着NumPy和SciPy提供的高阶数组和矩阵操作,Python成为科学计算应用的主流语言。NumPy实现了多维同质数组(homogeneous array)和矩阵,这些数据结构不但能处理数字,还能存放其他由用户定义的记录。通过NumPy,用户能对这些数据结构里的元素进行高效的操作。所以笔者一开始就接触的是Numpy,也没怎么用过array包。
    SciPy是基于NumPy的另一个库,它提供了很多跟科学计算有关的算法,专为线性代数、数值积分和统计学而设计。SciPy的高效和可靠性归功于其背后的C和Fortran代码,而这些跟计算有关的部分都源自于Netlib库。换句话说,SciPy把基于C和Fortran的工业级数学计算功能用交互式且高度抽象的Python包装起来,让科学家如鱼得水。

    我曾写过一个numpy 的简要教程:Python之Numpy基础 欢迎大家查看。

3、list.sort方法和内置函数sorted

list.sort方法会就地排序列表,原始的列表将会消失,这也是这个方法的返回值是None的原因。这其实是Python的一个惯例:如果一个函数或者方法对对象进行的是就地改动,那它就应该返回None,好让调用者知道传入的参数发生了变动,而且并未产生新的对象
与list.sort相反的是内置函数sorted,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。而不管sorted接受的是怎样的参数,它最后都会返回一个列表。
不管是list.sort方法还是sorted函数,都有两个可选的关键字参数。

  • reverse
    如果被设定为True,被排序的序列里的元素会以降序输出(也就是说把最大值当作最小值来排序)。这个参数的默认值是False。

  • key
    一个只有一个参数的函数,这个函数会被用在序列里的每一个元素上,所产生的结果将是排序算法依赖的对比关键字。比如说,在对一些字符串排序时,可以用key=str.lower来实现忽略大小写的排序,或者是用key=len进行基于字符串长度的排序,或者使用key=float对字符串数字进行排序。
    可选参数key还可以在内置函数min()和max()中起作用。另外,还有些标准库里的函数也接受这个参数,像itertools.groupby( )和heapq.nlargest( )等。

    ——本章完——
    欢迎关注我的微信公众号
    扫码关注公众号

你可能感兴趣的:(python技巧)