2.5.1 动态数组

NumPy的数组对象不能像列表一样动态地改变其大小,在做数据采集的时候,需要频繁地往数组中添加数据时很不方便。而Python标准库中的array数组提供了动态分配内存的功能,而且它和NumPy数组一样直接将数值的二进制数据保存在一块内存中,因此我们可以先用array数组收集数据,然后通过np.frombuffer( )将array数组的数据内存直接转换为NumPy数组。下面是一个例子:

    import numpy as np
    from array import array
    a = array("d", [1,2,3,4])   # 创建一个array数组
    # 通过np.frombuffer( )创建一个和a共享内存的NumPy数组
    na = np.frombuffer(a, dtype=np.float) 
    print a
    print na
    na[1] = 20  # 修改NumPy数组中下标为1的元素
    print a
    array('d', [1.0, 2.0, 3.0, 4.0])
    [ 1.  2.  3.  4.]
    array('d', [1.0, 20.0, 3.0, 4.0])

array数组只支持一维,如果我们需要采集多个通道的数据,可以将这些数据依次添加进array数组,然后通过reshape( )方法将np.frombuffer( )所创建的NumPy数组改为二维数组。在下面的例子中,我们通过array数组buf采集两个通道的数据,数据采集完毕之后,通过np.frombuffer( )将其转换为NumPy数组,并通过reshape( )将其形状改为二维数组:

    import math
    buf = array("d")
    for i in range(5):
        buf.append(math.sin(i*
0.1)) 
        buf.append(math.cos(i*
0.1))
    
    data = np.frombuffer(buf, dtype=np.float).reshape(-1, 2)
    print data
    [[ 0.          1.        ]
     [ 0.09983342  0.99500417]
     [ 0.19866933  0.98006658]
     [ 0.29552021  0.95533649]
     [ 0.38941834  0.92106099]]

下面是Python中实现array对象动态添加元素的算法:

●array对象拥有一块用于保存数据的内存,其长度通常比数组中的所有数据的字节数要长。

●当往array中添加数据时,如果数据内存中还有空余位置,则直接写入空余位置。

●当数据内存中无空余位置时,则重新分配一块更大的数据内存,并将当前的数据都复制到这块新的数据内存中,而旧的数据内存则被释放掉。

根据上述算法可知,只要往array中添加元素,其数据内存的地址就可能发生改变。在此之前通过np.frombuffer( )创建的数组仍然引用旧的数据内存,从而成为“野指针”。下面的代码演示了这个过程。其中array.buffer_info( )获得数据内存的地址以及其中有效数据的个数。

    a = array("d")
    for i in range(10):
        a.append(i)
        if i == 2:
            na = np.frombuffer(a, dtype=float)
        print a.buffer_info( ),
        if i == 4:
            print
    (83088512, 1) (83088512, 2) (83088512, 3) (83088512, 4) (31531848, 5)
    (31531848, 6) (31531848, 7) (31531848, 8) (34405776, 9) (34405776, 10)

由上面的结果可知,当数组a的长度为5和9时,数据内存被重新分配了。而na数组是在a的长度为3时通过np.frombuffer( )得到的,因此它的数据指针已经成为野指针。ndarray.ctypes.data可以获得数组的数据内存的地址,可以看出na的数据内存地址仍然是a在重新分配之前的地址,而na中的数据也变成了随机的无效数据。

    print na.ctypes.data
    print na
    83088512
    [  2.11777767e+161   6.24020631e-085   8.82069697e+199]

由上面的分析可知,每次动态数组的长度改变时,我们都需要重新调用np.frombuffer( )以创建一个新的ndarray数组对象来访问其中的数据。

当每个通道的数据类型不同时,就不能采用array.array对象了。这时可以使用bytearray收集数据。bytearray是字节数组,因此首先需要通过struct模块将Python的数值转换成其字节表示形式。如果数据来自二进制文件或硬件,那么很可能得到的已经是字节数据了,这个步骤可以省略。下面是使用bytearray进行数据采集的例子:

bytearray对象的+=运算与其extend( )方法的功能相同,但+=的运行速度要比extend( )快许多,读者可以使用%timeit自行验证。

    import struct
    buf = bytearray( )
    for i in range(5):
        buf += struct.pack("=hdd", i, math.sin(i*
0.1), math.cos(i*
0.1)) ❶
    
    dtype = np.dtype({"names":["id","sin","cos"], "formats":["h", "d", "d"]}) ❷
    data = np.frombuffer(buf, dtype=dtype) ❸
    print data
    [(0, 0.0, 1.0) (1, 0.09983341664682815, 0.9950041652780258)
     (2, 0.19866933079506122, 0.9800665778412416)
     (3, 0.2955202066613396, 0.955336489125606)
     (4, 0.3894183423086505, 0.9210609940028851)]

❶采集三个通道的数据,其中通道1是短整型数,其类型符号为“h”,通道2和3为双精度浮点数,其类型符号为“d”。类型格式字符串中的“=”表示输出的字节数据不进行内存对齐。即一条数据的字节数为2+8+8=18,如果没有“=”,那么一条数据的字节数为8+8+8=24。

❷定义一个dtype对象来表示一条数据的结构,dtype对象默认不进行内存对齐。如果采集数据用的bytearray中的数据是内存对齐的话,只需要设置dtype( )的align参数为True即可。

❸最后通过np.frombuffer( )将bytearray转换为NumPy的结构数组。然后就可以通过data["id"]、data["sin"]和data["cos"]访问这三个通道的数据了。