
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"]访问这三个通道的数据了。