打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Python 数据分析——NumPy ndarray对象
userphoto

2023.07.15 吉林

关注

NumPy中使用ndarray对象表示数组,它是整个库的核心对象,NumPy中所有的函数都是围绕ndarray对象进行处理的。ndarray的结构并不复杂,但是功能却十分强大。不但可以用它高效地存储大量的数值元素,从而提高数组计算的运算速度,还能用它与各种扩展库进行数据交换。本节的内容可能会有些枯燥,但是为了打下一个良好的基础,让我们从深入理解ndarray对象开始学习Python科学计算之旅。

一、创建

首先需要创建数组才能对其进行运算和操作。可以通过给array()函数传递Python的序列对象来创建数组,如果传递的是多层嵌套的序列,将创建多维数组(下例中的变量c):

a = np.array([1, 2, 3, 4])b = np.array((5, 6, 7, 8))c = np.array([[1, 2, 3, 4], [4, 5, 6, 7], [7, 8, 9, 10]])b c------------ ------------------[5, 6, 7, 8] [[ 1, 2, 3, 4],[ 4, 5, 6, 7],[ 7, 8, 9, 10]]

数组的形状可以通过其shape属性获得,它是一个描述数组各个轴的长度的元组(tuple):

a.shape b.shape c.shape------- ------- -------(4,) (4,) (3, 4)

数组a的shape属性只有一个元素,因此它是一维数组。而数组c的shape属性有两个元素,因此它是二维数组,其中第0轴的长度为3,第1轴的长度为4。还可以通过修改数组的shape属性,在保持数组元素个数不变的情况下,改变数组每个轴的长度。下面的例子将数组c的shape属性改为(4,3),注意从(3,4)改为(4,3)并不是对数组进行转置,而只是改变每个轴的大小,数组元素在内存中的位置并没有改变:

c.shape = 4, 3carray([[ 1, 2, 3],[ 4, 4, 5],[ 6, 7, 7],[ 8, 9, 10]])

当设置某个轴的元素个数为-1时,将自动计算此轴的长度。由于数组c有12个元素,因此下面的程序将数组c的shape属性改成了(2,6):

c.shape = 2, -1carray([[ 1, 2, 3, 4, 4, 5],[ 6, 7, 7, 8, 9, 10]])

使用数组的reshape()方法,可以创建指定形状的新数组,而原数组的形状保持不变:

d = a.reshape((2,2)) # 也可以用a.reshape(2,2)d a-------- ------------[[1, 2], [1, 2, 3, 4][3, 4]]

数组a和d其实共享数据存储空间,因此修改其中任意一个数组的元素都会同时修改另一个数组的内容。注意在下面的例子中,数组d中的2也被改成了100:

a[1] = 100 # 将数组a的第1个元素改为100a d-------------------- ------------[ 1, 100, 3, 4] [[ 1, 100],[ 3, 4]]

二、元素类型

数组的元素类型可以通过dtype属性获得。在前面的例子中,创建数组所用的序列的元素都是整数,因此所创建的数组的元素类型是整型,并且是32位的长整型。这是因为笔者所使用的Python是32位的,如果使用64位的操作系统和Python,那么默认整数类型的长度为64位。

c.dtypedtype('int32')

可以通过dtype参数在创建数组时指定元素类型,注意float类型是64位的双精度浮点类型,而complex是128位的双精度复数类型:

在上面的例子中,传递给dtype参数的都是类型(type)对象,其中float和complex为Python内置的浮点数类型和复数类型,而np.int32是NumPy定义的新的数据类型—— 32位符号整数类型。

NumPy也有自己的浮点数类型:float16、float32、float64和float128。当使用float64作为dtype参数时,其效果和内置的float类型相同。

在需要指定dtype参数时,也可以传递一个字符串来表示元素的数值类型。NumPy中的每个数值类型都有几种字符串表示方式,字符串和类型之间的对应关系都存储在typeDict字典中。下面的程序获得与float64类型对应的所有键值:

[key for key, value in np.typeDict.items() if value is np.float64][12, 'd', 'float64', 'float_', 'float', 'f8', 'double', 'Float64']

完整的类型列表可以通过下面的语句得到,它将typeDict字典中所有的值转换为一个集合,从而去除其中的重复项:

上面显示的数值类型与数组的dtype属性是不同的对象。通过dtype对象的type属性可以获得与其对应的数值类型:

c.dtype.typenumpy.int32

通过NumPy的数值类型也可以创建数值对象,下面创建一个16位的符号整数对象,它与Python的整数对象不同的是,它的取值范围有限,因此计算200*200会溢出,得到一个负数,这一点与C语言的16位整数的结果相同:

a = np.int16(200)a*a-25536

另外值得注意的是,NumPy的数值对象的运算速度比Python的内置类型的运算速度慢很多,如果程序中需要大量地对单个数值运算,应当尽量避免使用NumPy的数值对象。下面比较了Python内置的float类型与NumPy的双精度浮点数值float64的乘法运算的速度:

v1 = 3.14v2 = np.float64(v1)%timeit v1*v1%timeit v2*v210000000 loops, best of 3: 70.1 ns per loop10000000 loops, best of 3: 178 ns per loop

使用astype()方法可以对数组的元素类型进行转换,下面将浮点数数组t1转换为32位整数数组,将双精度的复数数组t2转换成单精度的复数数组:

t1 = np.array([1, 2, 3, 4], dtype=np.float)t2 = np.array([1, 2, 3, 4], dtype=np.complex)t3 = t1.astype(np.int32)t4 = t2.astype(np.complex64)

三、自动生成数组

前面的例子都是先创建一个Python的序列对象,然后通过array()将其转换为数组,这样做显然效率不高。因此NumPy提供了很多专门用于创建数组的函数。下面的每个函数都有一些关键字参数,具体用法请查看函数说明。

arange()类似于内置函数range(),通过指定开始值、终值和步长来创建表示等差数列的一维数组,注意所得到的结果中不包含终值。例如下面的程序创建开始值为0、终值为1、步长为0.1的等差数组,注意终值1不在数组中:

np.arange(0, 1, 0.1)array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])

linspace()通过指定开始值、终值和元素个数来创建表示等差数列的一维数组,可以通过endpoint参数指定是否包含终值,默认值为True,即包含终值。下面两个例子分别演示了endpoint为True和False时的结果,注意endpoint的值会改变数组的等差步长:

np.linspace(0, 1, 10, endpoint=False) # 步长为1/10array([ 0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])

logspace()和linspace()类似,不过它所创建的数组是等比数列。下面的例子产生从10⁰到10²、有5个元素的等比数列,注意起始值0表示10⁰,而终值2表示10²:

基数可以通过base参数指定,其默认值为10。下面通过将base参数设置为2,并设置endpoint参数为False,创建一个比例为2^(1/12)的等比数组,此等比数组的比值是音乐中相差半音的两个音阶之间的频率比值,因此可以用它计算一个八度中所有半音的频率:

zeros()、ones()、empty()可以创建指定形状和类型的数组。其中empty()只分配数组所使用的内存,不对数组元素进行初始化操作,因此它的运行速度是最快的。下面的程序创建一个形状为(2,3)、元素类型为整数的数组,注意其中的元素值没有被初始化:

np.empty((2,3), np.int)array([[1078523331, 1065353216, 1073741824],[1077936128, 1082130432, 1084227584]])

而zeros()将数组元素初始化为0,ones()将数组元素初始化为1。下面创建一个长度为4、元素类型为整数的一维数组,并且元素全部被初始化为0:

np.zeros(4, np.int)array([0, 0, 0, 0])

full()将数组元素初始化为指定的值:

np.full(4, np.pi)array([ 3.14159265, 3.14159265, 3.14159265, 3.14159265])

此外,zeros_like()、ones_like()、empty_like()、full_like()等函数创建与参数数组的形状和类型相同的数组,因此zeros_like(a)和zeros(a.shape, a.dtype)的效果相同。

frombuffer()、fromstring()、fromfile()等函数可以从字节序列或文件创建数组。下面以fromstring()为例介绍它们的用法,先创建含8个字符的字符串s:

s = 'abcdefgh'

Python的字符串实际上是一个字节序列,每个字符占一个字节。因此如果从字符串s创建一个8位的整数数组,所得到的数组正好就是字符串中每个字符的ASCII编码:

np.fromstring(s, dtype=np.int8)array([ 97, 98, 99, 100, 101, 102, 103, 104], dtype=int8)

如果从字符串s创建16位的整数数组,那么两个相邻的字节就表示一个整数,把字节98和字节97当作一个16位的整数,它的值就是98*256+97 = 25185。可以看出,16位的整数是以低位字节在前(little-endian)的方式保存在内存中的。

print 98*256+97np.fromstring(s, dtype=np.int16)25185array([25185, 25699, 26213, 26727], dtype=int16)

如果把整个字符串转换为一个64位的双精度浮点数数组,那么它的值是:

np.fromstring(s, dtype=np.float)array([ 8.54088322e+194])

显然这个结果没有什么意义,但是如果我们用C语言的二进制方式写了一组double类型的数值到某个文件中,那就可以从此文件读取相应的数据,并通过fromstring()将其转换为float64类型的数组,或者直接使用fromfile()从二进制文件读取数据。

fromstring()会对字符串的字节序列进行复制,而使用frombuffer()创建的数组与原始字符串共享内存。由于字符串是只读的,因此无法修改所创建的数组的内容:

buf = np.frombuffer(s, dtype=np.int16)buf[1] = 10---------------------------------------------------------------------------ValueError Traceback (most recent call last)<ipython-input-52-f523db231ae5> in <module>()1 buf = np.frombuffer(s, dtype=np.int16)----> 2 buf[1] = 10ValueError: assignment destination is read-only

Python中还有一些类型也支持buffer接口,例如bytearray、array.array等。在后面的章节中,我们会介绍如何使用这些对象实现动态数组的功能。

还可以先定义一个从下标计算数值的函数,然后用fromfunction()通过此函数创建数组:

def func(i):return i % 4 + 1np.fromfunction(func, (10,))array([ 1., 2., 3., 4., 1., 2., 3., 4., 1., 2.])

fromfunction()的第一个参数是计算每个数组元素的函数,第二个参数指定数组的形状。因为它支持多维数组,所以第二个参数必须是一个序列。上例中第二个参数是长度为1的元组(10,),因此创建了一个有10个元素的一维数组。

下面的例子创建一个表示九九乘法表的二维数组,输出的数组a中的每个元素a[i, j]都等于func2(i, j):

def func2(i, j):return (i + 1) * (j + 1)np.fromfunction(func2, (9,9))array([[ 1., 2., 3., 4., 5., 6., 7., 8., 9.],[ 2., 4., 6., 8., 10., 12., 14., 16., 18.],[ 3., 6., 9., 12., 15., 18., 21., 24., 27.],[ 4., 8., 12., 16., 20., 24., 28., 32., 36.],[ 5., 10., 15., 20., 25., 30., 35., 40., 45.],[ 6., 12., 18., 24., 30., 36., 42., 48., 54.],[ 7., 14., 21., 28., 35., 42., 49., 56., 63.],[ 8., 16., 24., 32., 40., 48., 56., 64., 72.],[ 9., 18., 27., 36., 45., 54., 63., 72., 81.]])

四、存取元素

可以使用和列表相同的方式对数组的元素进行存取:

a = np.arange(10)aarray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

· a[5]:用整数作为下标可以获取数组中的某个元素。

· a[3:5]:用切片作为下标获取数组的一部分,包括a[3]但不包括a[5]。

· a[:5]:切片中省略开始下标,表示从a[0]开始。

· a[:-1]:下标可以使用负数,表示从数组最后往前数。

· a[1:-1:2]:切片中的第三个参数表示步长,2表示隔一个元素取一个元素。

· a[::-1]:省略切片的开始下标和结束下标,步长为-1,整个数组头尾颠倒。

· a[5:1:-2]:步长为负数时,开始下标必须大于结束下标。

下标还可以用来修改元素的值:

a[2:4] = 100, 101aarray([ 0, 1, 100, 101, 4, 5, 6, 7, 8, 9])

和列表不同的是,通过切片获取的新的数组是原始数组的一个视图。它与原始数组共享同一块数据存储空间。下面的程序将b的第2个元素修改为-10,a的第5个元素也同时被修改为-10,因为它们在内存中的地址相同。

b = a[3:7] # 通过切片产生一个新的数组b,b和a共享同一块数据存储空间b[2] = -10 # 将b的第2个元素修改为-10b a-------------------- --------------------------------------------------[101, 4, -10, 6] [ 0, 1, 100, 101, 4, -10, 6, 7, 8, 9]

除了使用切片下标存取元素之外,NumPy还提供了整数列表、整数数组和布尔数组等几种高级下标存取方法。

当使用整数列表对数组元素进行存取时,将使用列表中的每个元素作为下标。使用列表作为下标得到的数组不和原始数组共享数据:

x = np.arange(10, 1, -1)xarray([10, 9, 8, 7, 6, 5, 4, 3, 2])

· x[[3, 3, 1, 8]]:获取x中的下标为3、3、1、8的4个元素,组成一个新的数组。

· x[[3, 3, -3, 8]]:下标可以是负数,-3表示取倒数第3个元素(从1开始计数)。

a = x[[3, 3, 1, 8]]b = x[[3, 3, -3, 8]]a b------------ ------------[7, 7, 9, 2] [7, 7, 4, 2]

下面修改b[2]的值,但是由于它和x不共享内存,因此x的值不变:

b[2] = 100b x-------------------- ------------------------------------[ 7, 7, 100, 2] [10, 9, 8, 7, 6, 5, 4, 3, 2]

整数序列下标也可以用来修改元素的值:

x[[3,5,1]] = -1, -2, -3xarray([10, -3, 8, -1, 6, -2, 4, 3, 2])

当使用整数数组作为数组下标时,将得到一个形状和下标数组相同的新数组,新数组的每个元素都是用下标数组中对应位置的值作为下标从原数组获得的值。当下标数组是一维数组时,结果和用列表作为下标的结果相同:

x = np.arange(10,1,-1)x[np.array([3,3,1,8])]array([7, 7, 9, 2])

而当下标是多维数组时,得到的也是多维数组:

x[np.array([[3,3,1,8],[3,3,-3,8]])]array([[7, 7, 9, 2],[7, 7, 4, 2]])

可以将上述操作理解为:先将下标数组展平为一维数组,并作为下标获得一个新的一维数组,然后将其形状修改为下标数组的形状:

x[[3,3,1,8,3,3,-3,8]].reshape(2,4) # 改变数组形状array([[7, 7, 9, 2],[7, 7, 4, 2]])

当使用布尔数组b作为下标存取数组x中的元素时,将获得数组x中与数组b中True对应的元素。使用布尔数组作为下标获得的数组不和原始数组共享数据内存,注意这种方式只对应于布尔数组,不能使用布尔列表。

x = np.arange(5,0,-1)xarray([5, 4, 3, 2, 1])

布尔数组中下标为0,2的元素为True,因此获取x中下标为0, 2的元素:

x[np.array([True, False, True, False, False])]array([5, 3])

如果是布尔列表,就把True当作1,把False当作0,按照整数序列方式获取x中的元素:

x[[True, False, True, False, False]]array([4, 5, 4, 5, 5])

在NumPy 1.10之后的版本中,布尔列表会被当作布尔数组,因此上面的运行结果会变成array([5, 3])。

布尔数组的长度不够时,不够的部分都当作False:

x[np.array([True, False, True, True])]array([5, 3, 2])

布尔数组的下标也可以用来修改元素:

x[np.array([True, False, True, True])] = -1, -2, -3xarray([-1, 4, -2, -3, 1])

布尔数组一般不是手工产生,而是使用布尔运算的ufunc函数产生,关于ufunc函数请参照下一节的介绍。下面我们举一个简单的例子说明布尔数组下标的用法:

x = np.random.randint(0, 10, 6) # 产生一个长度为6,元素值为0到9的随机整数数组x x > 5------------------ ------------------------------------------[8, 1, 5, 6, 2, 7] [ True, False, False, True, False, True]

表达式x > 5将数组x中的每个元素和5进行大小比较,得到一个布尔数组,True表示x中对应的值大于5。我们可以使用x > 5所得到的布尔数组收集x中所有大于5的数值:

x[x > 5]array([8, 6, 7])

五、多维数组

多维数组的存取和一维数组类似,因为多维数组有多个轴,所以它的下标需要用多个值来表示。NumPy采用元组作为数组的下标,元组中的每个元素和数组的每个轴对应。图1显示了一个shape为(6, 6)的数组a,图中用不同颜色和线型标出各个下标所对应的选择区域。

图1 使用数组切片语法访问多维数组中的元素

为什么使用元组作为下标

Python的下标语法(用[]存取序列中的元素)本身并不支持多维,但是可以使用任何对象作为下标,因此NumPy使用元组作为下标存取数组中的元素,使用元组可以很方便地表示多个轴的下标。虽然在Python程序中经常用圆括号将元组的元素括起来,但其实元组的语法只需要用逗号隔开元素即可,例如x, y = y, x就是用元组交换变量值的一个例子。因此a[1, 2]和a[(1, 2)]完全相同,都是使用元组(1,2)作为数组a的下标。

读者也许会对如何创建图中的二维数组感到好奇。它实际上是一个加法表,由纵向量(0, 10, 20, 30, 40, 50)和横向量(0, 1, 2, 3, 4, 5)的元素相加而得。可以用下面的语句创建它。

a = np.arange(0, 60, 10).reshape(-1, 1) + np.arange(0, 6)aarray([[ 0, 1, 2, 3, 4, 5],[10, 11, 12, 13, 14, 15],[20, 21, 22, 23, 24, 25],[30, 31, 32, 33, 34, 35],[40, 41, 42, 43, 44, 45],[50, 51, 52, 53, 54, 55]])

图1中的下标都是有两个元素的元组,其中的第0个元素与数组的第0轴(纵轴)对应,而第1个元素与数组的第1轴(横轴)对应。下面是图中各种多维数组切片的运算结果:

如果下标元组中只包含整数和切片,那么得到的数组和原始数组共享数据,它是原数组的视图。下面的例子中,数组b是a的视图,它们共享数据,因此修改b[0]时,数组a中对应的元素也被修改:

b = a[0, 3:5]b[0] = -b[0]a[0, 3:5]array([-3, 4])

因为数组的下标是一个元组,所以我们可以将下标元组保存起来,用同一个元组存取多个数组。在下面的例子中,a[idx]和a[::2,2:]相同,a[idx][idx]和a[::2,2:][::2,2:]相同。

idx = slice(None, None, 2), slice(2,None)a[idx] a[idx][idx]-----------------------------[[ 2, -3, 4, 5], [[ 4, 5],[22, 23, 24, 25], [44, 45]][42, 43, 44, 45]]

切片(slice)对象

根据Python的语法,在[]中可以使用以冒号隔开的两个或三个整数表示切片,但是单独生成切片对象时需要使用slice()来创建。它有三个参数,分别为开始值、结束值和间隔步长,当这些值需要省略时可以使用None。例如,a[slice(None,None,None),2]和a[:, 2]相同。

用Python的内置函数slice()创建下标比较麻烦,因此NumPy提供了一个s_对象来帮助我们创建数组下标,请注意s_实际上是IndexExpression类的一个对象:

np.s_[::2, 2:](slice(None, None, 2), slice(2, None, None))

s_为什么不是函数

根据Python的语法,只有在中括号[]中才能使用以冒号隔开的切片语法,如果s_是函数,那么这些切片必须使用slice()创建。类似的对象还有mgrid和ogrid等,后面我们会学习它们的用法。Python的下标语法实际上会调用__getitem__()方法,因此我们可以很容易自己实现s_对象的功能:

class S(object):def __getitem__(self, index):return index

在多维数组的下标元组中,也可以使用整数元组或列表、整数数组和布尔数组,如图2所示。当下标中使用这些对象时,所获得的数据是原始数据的副本,因此修改结果数组不会改变原始数组。

图2 使用整数序列和布尔数组访问多维数组中的元素

在a[(0,1,2,3),(1,2,3,4)]中,下标仍然是一个有两个元素的元组,元组中的每个元素都是一个整数元组,分别对应数组的第0轴和第1轴。从两个序列的对应位置取出两个整数组成下标,于是得到的结果是:a[0,1]、a[1,2]、a[2,3]、a[3,4]。

a[(0,1,2,3),(1,2,3,4)]array([ 1, 12, 23, 34])

在a[3:, [0,2,5]]中,第0轴的下标是一个切片对象,它选取第3行之后的所有行;第1轴的下标是整数列表,它选取第0、第2和第5列。

a[3:, [0,2,5]]array([[30, 32, 35],[40, 42, 45],[50, 52, 55]])

在a[mask, 2]中,第0轴的下标是一个布尔数组,它选取第0、第2和第5行;第1轴的下标是一个整数,它选取第2列。

mask = np.array([1,0,1,0,0,1], dtype=np.bool)a[mask, 2]array([ 2, 22, 52])

注意,如果mask不是布尔数组而是整数数组、列表或元组,就按照以整数数组作为下标的方式进行运算:

mask1 = np.array([1,0,1,0,0,1])mask2 = [True,False,True,False,False,True]a[mask1, 2] a[mask2, 2]------------------------ ------------------------[12, 2, 12, 2, 2, 12] [12, 2, 12, 2, 2, 12]

当下标的长度小于数组的维数时,剩余的各轴所对应的下标是“:”,即选取它们的所有数据:

a[[1,2],:] a[[1,2]]-------------------------- --------------------------[[10, 11, 12, 13, 14, 15], [[10, 11, 12, 13, 14, 15],[20, 21, 22, 23, 24, 25]] [20, 21, 22, 23, 24, 25]]

当所有轴都用形状相同的整数数组作为下标时,得到的数组和下标数组的形状相同:

x = np.array([[0,1],[2,3]])y = np.array([[-1,-2],[-3,-4]])a[x,y]array([[ 5, 14],[23, 32]])

效果和下面的程序相同:

a[(0,1,2,3),(-1,-2,-3,-4)].reshape(2,2)array([[ 5, 14],[23, 32]])

当没有指定第1轴的下标时,使用“:”作为下标,因此得到了一个三维数组:

a[x]array([[[ 0, 1, 2, -3, 4, 5],[10, 11, 12, 13, 14, 15]],[[20, 21, 22, 23, 24, 25],[30, 31, 32, 33, 34, 35]]])

可以使用这种以整数数组作为下标的方式快速替换数组中的每个元素,例如有一个表示索引图像的数组image,以及一个调色板数组palette,则palette[image]可以得到通过调色板着色之后的彩色图像:

palette = np.array( [ [0,0,0],[255,0,0],[0,255,0],[0,0,255],[255,255,255] ] )image = np.array( [ [ 0, 1, 2, 0 ],[ 0, 3, 4, 0 ] ] )palette[image]array([[[ 0, 0, 0],[255, 0, 0],[ 0, 255, 0],[ 0, 0, 0]],[[ 0, 0, 0],[ 0, 0, 255],[255, 255, 255],[ 0, 0, 0]]])

六、结构数组

在C语言中我们可以通过struct关键字定义结构类型,结构中的字段占据连续的内存空间。类型相同的两个结构所占用的内存大小相同,因此可以很容易定义结构数组。和C语言一样,在NumPy中也很容易对这种结构数组进行操作。只要NumPy中的结构定义和C语言中的结构定义相同,就可以很方便地读取C语言的结构数组的二进制数据,将其转换为NumPy的结构数组。

假设我们需要定义一个结构数组,它的每个元素都有name、age和weight字段。在NumPy中可以如下定义:

persontype = np.dtype({ ❶'names':['name', 'age', 'weight'],'formats':['S30','i', 'f']}, align=True)a = np.array([('Zhang', 32, 75.5), ('Wang', 24, 65.2)], ❷dtype=persontype)

❶我们先创建一个dtype对象persontype,它的参数是一个描述结构类型的各个字段的字典。字典有两个键:'names'和'formats'。每个键对应的值都是一个列表。'names'定义结构中每个字段的名称,而'formats'则定义每个字段的类型。这里我们使用类型字符串定义字段类型:

· 'S30':长度为30个字节的字符串类型,由于结构中的每个元素的大小必须固定,因此需要指定字符串的长度。

· 'i':32位的整数类型,相当于np.int32。

· 'f':32位的单精度浮点数类型,相当于np.float32。

❷然后调用array()以创建数组,通过dtype参数指定所创建的数组的元素类型为persontype。下面查看数组a的元素类型:

a.dtypedtype({'names':['name','age','weight'], 'formats':['S30','<i4','<f4'],'offsets':[0,32,36], 'itemsize':40}, align=True)

还可以用包含多个元组的列表来描述结构的类型:

dtype([('name', '|S30'), ('age', '<i4'), ('weight', '<f4')])

其中形如“(字段名, 类型描述)”的元组描述了结构中的每个字段。类型字符串前面的'|'、'<'、'>'等字符表示字段值的字节顺序:

· |:忽视字节顺序。

· lt;:低位字节在前,即小端模式(little endian)。

· >:高位字节在前,即大端模式(big endian)。

结构数组的存取方式和一般数组相同,通过下标能够取得其中的元素,注意元素的值看上去像是元组,实际上是结构:

print a[0]a[0].dtype('Zhang', 32, 75.5)dtype({'names':['name','age','weight'], 'formats':['S30','<i4','<f4'],'offsets':[0,32,36], 'itemsize':40}, align=True)

我们可以使用字段名作为下标获取对应的字段值:

a[0]['name']'Zhang'

a[0]是一个结构元素,它和数组a共享内存数据,因此可以通过修改它的字段来改变原始数组中对应元素的字段:

c = a[1]c['name'] = 'Li'a[1]['name']'Li'

我们不但可以获得结构元素的某个字段,而且可以直接获得结构数组的字段,返回的是原始数组的视图,因此可以通过修改b[0]来改变a[0]['age']:

b=a['age']b[0] = 40print a[0]['age']40

通过a.tostring()或a.tofile()方法,可以将数组a以二进制的方式转换成字符串或写入文件:

a.tofile('test.bin')

利用下面的C语言程序可以将test.bin文件中的数据读取出来。%%file为IPython的魔法命令,它将该单元格中的文本保存成文件read_struct_array.c:

%%file read_struct_array.c#include <stdio.h>struct person{char name[30];int age;float weight;};struct person p[3];void main (){FILE *fp;int i;fp=fopen('test.bin','rb');fread(p, sizeof(struct person), 2, fp);fclose(fp);for(i=0;i<2;i++){printf('%s %d %f\n', p[i].name, p[i].age, p[i].weight);}}

在IPython中可以通过!执行系统命令,下面调用gcc编译前面的C语言程序并执行:

!gcc read_struct_array.c -o read_struct_array.exe!read_struct_array.exeZhang 40 75.500000Li 24 65.199997

内存对齐

为了内存寻址方便,C语言的结构类型会自动添加一些填充用的字节,这叫做内存对齐。例如上面C语言中定义的结构的name字段虽然是30个字节长,但是由于内存对齐问题,在name和age中间会填补两个字节。因此,如果数组中所配置的内存大小不符合C语言的对齐规范,将会出现数据错位。为了解决这个问题,在创建dtype对象时,可以传递参数align=True,这样结构数组的内存对齐就和C语言的结构类型一致了。在前面的例子中,由于创建persontype时指定align参数为True,因此它占用40个字节。

结构类型中可以包括其他的结构类型,下面的语句创建一个有一个字段f1的结构,f1的值是另一个结构,它有字段f2,类型为16位整数:

np.dtype([('f1', [('f2', np.int16)])])dtype([('f1', [('f2', '<i2')])])

当某个字段类型为数组时,用元组的第三个元素表示其形状。在下面的结构体中,f1字段是一个形状为(2, 3)的双精度浮点数组:

np.dtype([('f0', 'i4'), ('f1', 'f8', (2, 3))])dtype([('f0', '<i4'), ('f1', '<f8', (2, 3))])

用下面的字典参数也可以定义结构类型,字典的键为结构的字段名,值为字段的类型描述。但是由于字典的键是没有顺序的,因此字段的顺序需要在类型描述中给出。类型描述是一个元组,它的第二个值给出字段的以字节为单位的偏移量,例如下例中的age字段的偏移量为25个字节:

np.dtype({'surname':('S25',0),'age':(np.uint8,25)})dtype([('surname', 'S25'), ('age', 'u1')])

七、内存结构

下面让我们看看数组对象是如何在内存中存储的。如图3所示,数组的描述信息保存在一个数据结构中,这个结构引用两个对象:用于保存数据的存储区域和用于描述元素类型的dtype对象。

图3 ndarray数组对象在内存中的存储方式

数据存储区域保存着数组中所有元素的二进制数据,dtype对象则知道如何将元素的二进制数据转换为可用的值。数组的维数和形状等信息都保存在ndarray数组对象的数据结构中。图3中显示的是下面的数组a的内存结构:

a = np.array([[0,1,2],[3,4,5],[6,7,8]], dtype=np.float32)

数组对象使用strides属性保存每个轴上相邻两个元素的地址差,即当某个轴的下标增加1时,数据存储区中的指针所增加的字节数。例如图2-3中的strides为(12,4),即第0轴的下标增加1时,数据的地址增加12个字节。也就是a[1,0]的地址比a[0,0]的地址大12,正好是3个单精度浮点数的总字节数。第1轴的下标增加1时,数据的地址增加4个字节,正好是一个单精度浮点数的字节数。

如果strides属性中的数值正好和对应轴所占据的字节数相同,那么数据在内存中是连续存储的。通过切片下标得到的新数组是原始数组的视图,即它和原始数组共享数据存储区域,但是新数组的strides属性会发生变化:

b = a[::2, ::2]b b.strides------------ ---------[[ 0., 2.], (24, 8)[ 6., 8.]]

由于数组b和数组a共享数据存储区,而数组b中的第0轴和第1轴都是从a中隔一个元素取一个,因此数组b的strides变成了(24, 8),正好都是数组a的两倍。对照前面的图2-3很容易看出数据0和2的地址相差8个字节,而数据0和6的地址相差24个字节。

元素在数据存储区中的排列格式有两种:C语言格式和Fortran语言格式。在C语言中,多维数组的第0轴是最上位的,即第0轴的下标增加1时,元素的地址增加的字节数最多;而Fortran语言中的多维数组的第0轴是最下位的,即第0轴的下标增加1时,地址只增加一个元素的字节数。在NumPy中默认以C语言格式存储数据,如果希望改为Fortran格式,只需要在创建数组时,设置order参数为'F':

c = np.array([[0,1,2],[3,4,5],[6,7,8]], dtype=np.float32, order='F')c.strides(4, 12)

了解了数组的内存结构,就可以解释使用下标取得数据时的复制和引用问题:

· 当下标使用整数和切片时,所取得的数据在数据存储区域中是等间隔分布的。因为只需要修改图2-3所示的数据结构中的dim count、dimensions、stride等属性以及指向数据存储区域的指针data,就能实现整数和切片下标,所以新数组和原始数组能够共享数据存储区域。

· 当使用整数序列、整数数组和布尔数组时,不能保证所取得的数据在数据存储区域中是等间隔的,因此无法和原始数组共享数据,只能对数据进行复制。

数组的flags属性描述了数据存储区域的一些属性,直接查看flags属性将输出各个标志的值,也可以单独获得其中的某个标志值:

print a.flagsprint 'c_contiguous:', a.flags.c_contiguousC_CONTIGUOUS : TrueF_CONTIGUOUS : FalseOWNDATA : TrueWRITEABLE : TrueALIGNED : TrueUPDATEIFCOPY : Falsec_contiguous: True

下面是几个比较重要的标志:

· C_CONTIGUOUS:数据存储区域是否是C语言格式的连续区域。

· F_CONTIGUOUS:数据存储区域是否是Fortran语言格式的连续区域。

· OWNDATA:数组是否拥有此数据存储区域,当一个数组是其他数组的视图时,它不拥有数据存储区域。

由于数组a是通过array()直接创建的,因此它的数据存储区域是C语言格式的连续区域,并且它拥有数据存储区域。下面我们看看数组a的转置标志,数组的转置可以通过其T属性获得,转置数组将其数据存储区域看作Fortran语言格式的连续区域,并且它不拥有数据存储区域。

a.T.flagsC_CONTIGUOUS : FalseF_CONTIGUOUS : TrueOWNDATA : FalseWRITEABLE : TrueALIGNED : TrueUPDATEIFCOPY : False

下面查看数组b的标志,它不拥有数据存储区域,其数据也不是连续存储的。通过视图数组的base属性可以获得保存数据的原始数组:

b.flagsC_CONTIGUOUS : FalseF_CONTIGUOUS : FalseOWNDATA : FalseWRITEABLE : TrueALIGNED : TrueUPDATEIFCOPY : Falseid(b.base) id(a)---------- ---------119627272 119627272

我们还可以通过view()方法从同一块数据区创建不同的dtype的数组对象,也就是使用不同的数值类型查看同一段内存中的二进制数据:

由于数组a的元素类型是单精度浮点数,占用4个字节,通过a.view(np.uint32),我们创建了一个新的数组,它和数组a使用同一段数据内存,但是它将每4个字节的数据当作无符号32位整数处理。而a.view(np.uint8)将每个字节都当作一个单字节的无符号整数,因此得到一个形状为(3, 8)的数组。通过view()方法获得的新数组与原数组共享内存,当a[0, 0]被修改时,b[0, 0]和c[0, :4]都会改变:

a[0, 0] = 3.14b[0, 0] c[0, :4]---------- --------------------1078523331 [195, 245, 72, 64]
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
NumPy
Python最牛逼数据分析库!月薪35K大牛:整理的NumPy详细教程!
Python科学运算之存取元素
Python 机器学习库 NumPy 教程
Python numpy中矩阵的基本用法汇总
Python之路
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服