ndarray对象

函数库的导入

本书的示例程序假设用以下推荐的方式导入NumPy函数库:

import numpy as np

创建

NumPy的函数和方法都有详细的说明文档和用法示例。在IPython中输入函数名并添加一个“?”号即可以显示文档内容,例如输入:“np.array?”可以查看array()的说明。

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

02-numpy/numpy_intro.py

NumPy的基本使用方法

>>> 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
array([5, 6, 7, 8])
>>> c
array([[1, 2, 3, 4],
       [4, 5, 6, 7],
       [7, 8, 9, 10]])

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

>>> a.shape
(4,)
>>> c.shape
(3, 4)

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

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

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

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

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

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

数组a和d其实共享数据存储空间,因此修改其中任意一个数组的元素都会同时修改另外一个数组的内容:

>>> a[1] = 100 # 将数组a的第一个元素改为100
>>> d # 注意数组d中的2也被改为了100
array([[  1, 100],
       [  3,   4]])

数组的元素类型可以通过dtype属性获得。前面例子中,创建数组所用的序列的元素都是整数,因此所创建的数组的元素类型是整型,并且是32bit的长整型:

>>> c.dtype
dtype('int32')

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

>>> np.array([1, 2, 3, 4], dtype=np.float)
array([  1.,   2.,   3.,   4.])
>>> np.array([1, 2, 3, 4], dtype=np.complex)
array([  1.+0.j,   2.+0.j,   3.+0.j,   4.+0.j])

NumPy中的数据类型都有几种字符串表示方式,字符串和类型之间的对应关系都储存在typeDict字典中,例如’d’、 ‘double’、’float64’都表示双精度浮点数类型:

>>> np.typeDict["d"]
<type 'numpy.float64'>
>>> np.typeDict["double"]
<type 'numpy.float64'>
>>> np.typeDict["float64"]
<type 'numpy.float64'>

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

>>> set(np.typeDict.values())
set([<type 'numpy.bool_'>     ,<type 'numpy.int8'>      ,<type 'numpy.int16'>
     <type 'numpy.float32'>   ,<type 'numpy.uint8'>     ,<type 'numpy.complex128'>
     <type 'numpy.unicode_'>  ,<type 'numpy.uint64'>    ,<type 'numpy.int64'>
     <type 'numpy.complex64'> ,<type 'numpy.string_'>   ,<type 'numpy.uint32'>
     <type 'numpy.void'>      ,<type 'numpy.int32'>     ,<type 'numpy.float96'>
     <type 'numpy.object_'>   ,<type 'numpy.uint32'>    ,<type 'numpy.int32'>
     <type 'numpy.float64'>   ,<type 'numpy.complex192'>,<type 'numpy.uint16'> ])

前面的例子都是先创建一个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) # 步长为1/9
array([ 0.        ,  0.11111111,  0.22222222,  0.33333333,  0.44444444,
        0.55555556,  0.66666667,  0.77777778,  0.88888889,  1.        ])
>>> np.linspace(0, 1, 10, endpoint=False) # 步长为1/10
array([ 0. ,  0.1,  0.2,  0.3,  0.4,  0.5,  0.6,  0.7,  0.8,  0.9])

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

>>> np.logspace(0, 2, 5)
array([   1.        ,    3.16227766,   10.        ,   31.6227766 ,  100.        ])

基数可以通过base参数指定,其缺省值为10。下面通过将base参数设置为2,并设置endpoint参数为True,创建一个比例为{2}^{1/12}的等比数组[1]

>>> np.logspace(0, 1, 12, base=2, endpoint=False)
array([ 1.        ,  1.05946309,  1.12246205,  1.18920712,  1.25992105,
        1.33483985,  1.41421356,  1.49830708,  1.58740105,  1.68179283,
        1.78179744,  1.88774863])

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

>>> np.empty((2,3),np.int)  #只分配内存,不对其进行初始化
array([[  32571594,   32635312,  505219724],
       [  45001384, 1852386928,     665972]])

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

>>> np.zeros(4, np.float)  #元素类型的缺省值为np.float,因此这里可以省略
array([ 0.,  0.,  0.,  0.])

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

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

>>> s = "abcdefgh"

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

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

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

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

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

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

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

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

>>> def func(i):
...   return i%4+1
...
>>> np.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)
...
>>> a = np.fromfunction(func2, (9,9))
>>> a
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.]])

Footnotes

[1]此等比数组的比值是音乐中相差半音的两个音阶之间的频率比值,因此可以用它计算一个八度中所有半音的频率。

存取元素

02-numpy/numpy_access1d.py

一维数组的元素存取

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

>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a[5]    # 用整数作为下标可以获取数组中的某个元素
5
>>> a[3:5]  # 用切片作为下标获取数组的一部分,包括a[3]不包括a[5]
array([3, 4])
>>> a[:5]   # 切片中省略开始下标,表示从a[0]开始
array([0, 1, 2, 3, 4])
>>> a[:-1]  # 下标可以使用负数,表示从数组最后往前数
array([0, 1, 2, 3, 4, 5, 6, 7, 8])
>>> a[2:4] = 100,101    # 下标还可以用来修改元素的值
>>> a
array([  0,   1, 100, 101,   4,   5,   6,   7,   8,   9])
>>> a[1:-1:2]   # 切片中的第三个参数表示步长,2表示隔一个元素取一个元素
array([  1, 101,   5,   7])
>>> a[::-1] # 省略切片的开始下标和结束下标,步长为-1,整个数组头尾颠倒
array([  9,   8,   7,   6,   5,   4, 101, 100,   1,   0])
>>> a[5:1:-2] # 步长为负数时,开始下标必须大于结束下标
array([  5, 101])

和列表不同的是,通过切片获取的新的数组是原始数组的一个视图。它与原始数组共享同一块数据存储空间:

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

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

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

>>> x = np.arange(10,1,-1)
>>> x
array([10,  9,  8,  7,  6,  5,  4,  3,  2])
>>> x[[3, 3, 1, 8]] # 获取x中的下标为3, 3, 1, 8的4个元素,组成一个新的数组
array([7, 7, 9, 2])
>>> b = x[[3,3,-3,8]]  #下标可以是负数
>>> b[2] = 100
>>> b
array([7, 7, 100, 2])
>>> x   # 由于b和x不共享数据空间,因此x中的值并没有改变
array([10,  9,  8,  7,  6,  5,  4,  3,  2])
>>> x[[3,5,1]] = -1, -2, -3 # 整数序列下标也可以用来修改元素的值
>>> x
array([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)
>>> x
array([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])
>>> # 布尔数组的长度不够时,不够的部分都当作False
>>> x[np.array([True, False, True, True])]
array([5, 3, 2])
>>> # 布尔数组下标也可以用来修改元素
>>> x[np.array([True, False, True, True])] = -1, -2, -3
>>> x
array([-1,  4, -2, -3,  1])

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

>>> x = np.random.rand(10) # 产生一个长度为10,元素值为0到1的随机数组
>>> x
array([ 0.72223939,  0.921226  ,  0.7770805 ,  0.2055047 ,  0.17567449,
        0.95799412,  0.12015178,  0.7627083 ,  0.43260184,  0.91379859])
>>> x>0.5
>>> # 数组x中的每个元素和0.5进行大小比较,得到一个布尔数组,True表示x中对应的值大于0.5
array([ True, True, True, False, False, True, False, True, False, True], dtype=bool)
>>> # 使用x>0.5所得到的布尔数组收集x中的元素,因此结果就是包含x中所有大于0.5的元素的数组
>>> x[x>0.5]
array([ 0.72223939,  0.921226  ,  0.7770805 ,  0.95799412,  0.7627083 , 0.91379859])

多维数组

多维数组的存取和一维数组类似,因为多维数组有多个轴,因此它的下标需要用多个值表示。NumPy采用元组(tuple)作为数组的下标,元组中的每个元素和数组的每个轴相对应。【图:使用数组切片语法访问多维数组中的元素】显示了一个形状为(6,6)的数组a,图中用不同颜色和线型标示出各个下标所对应的选择区域。

02-numpy/numpy_access2d.py

多维数组的元素存取

为什么使用元组作为下标

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

/tech/static/books/scipy/_images//numpy_access2d_01.png

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

读者也许会对如何创建【图:使用数组切片语法访问多维数组中的元素】中的二维数组感到好奇。它实际上是一个加法表,由纵向量(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)
>>> a
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],
       [40, 41, 42, 43, 44, 45],
       [50, 51, 52, 53, 54, 55]])

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

>>> a[0,3:5]
array([3, 4])
>>> a[4:,4:]
array([[44, 45],
       [54, 55]])
>>> a[:,2]
array([ 2, 12, 22, 32, 42, 52])
>>> a[2::2,::2]
array([[20, 22, 24],
       [40, 42, 44]])

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

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

因为数组的下标是一个元组,所以我们可以将下标元组保存起来,用同一个元组存取多个数组:

>>> idx = slice(None, None, 2), slice(2,None)
>>> a[idx] # 和a[::2,2:]相同
array([[ 2, -3,  4,  5],
       [22, 23, 24, 25],
       [42, 43, 44, 45]])
>>> a[idx][idx] # 和a[::2,2:][::2,2:]相同
array([[ 4,  5],
       [44, 45]])

slice对象

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

用Python的内置slice()函数创建下标比较麻烦,因此NumPy提供了一个s_对象帮助我们创建数组下标:

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

请注意s_实际上是一个IndexExpression类的对象:

>>> np.s_
<numpy.lib.index_tricks.IndexExpression object at 0x015093D0>

s_为什么不是函数

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

>>> class S(object):
...     def __getitem__(self, index):
...         return index
>>> S()[::2,2:]
(slice(None, None, 2), slice(2, None, None))

在多维数组的下标元组中,也可以使用整数元组或列表、整数数组和布尔数组。当下标中使用这些对象时,所获得的数据是原始数据的拷贝,因此修改结果数组不会改变原始数组。【图:使用整数序列和布尔数组访问多维数组中的元素】显示使用各种序列下标存取多维数组。

/tech/static/books/scipy/_images//numpy_access2d_02.png

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

>>> a[(0,1,2,3),(1,2,3,4)] #
array([ 1, 12, 23, 34])
>>> a[3:, [0,2,5]] #
array([[30, 32, 35],
       [40, 42, 45],
       [50, 52, 55]])
>>> mask = np.array([1,0,1,0,0,1], dtype=np.bool)
>>> a[mask, 2] #
array([ 2, 22, 52])

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

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

❸第0轴的下标是一个布尔数组,它选取第0、2、5行;第1轴的下标是一个整数,选取第2列。注意如果mask不是布尔数组而是整数数组、列表或者元组,则按照❶的方式进行运算:

>>> mask = np.array([1,0,1,0,0,1])
>>> a[mask, 2]
array([12,  2, 12,  2,  2, 12])
>>> mask = [True,False,True,False,False,True]
>>> a[mask, 2]
array([12,  2, 12,  2,  2, 12])

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

>>> a[[1,2]]  #与a[[1,2],:]相同
array([[10, 11, 12, 13, 14, 15],
       [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中可以如下定义:

02-numpy/numpy_struct_array.py

用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’则定义每个字段的类型。这里我们使用类型字符串定义字段类型:

  • ‘S32’ : 长度为32个字节的字符串类型,由于结构中的每个元素的大小必须固定,因此需要指定字符串的长度。
  • ‘i’ : 32bit的整数类型,相当于np.int32。
  • ‘f’ : 32bit的单精度浮点数类型,相当于np.float32。

❷然后调用array()创建数组,通过dtype参数指定所创建的数组的元素类型为persontype。运行上面程序之后,我们可以在IPython中执行如下的语句查看数组a的元素类型:

>>> run numpy_struct_array.py
>>> a.dtype
dtype([('name', '|S32'), ('age', '<i4'), ('weight', '<f4')])

这里我们看到了另外一种描述结构类型的方法: 一个包含多个元组的列表,其中形如 (字段名, 类型描述) 的元组描述了结构中的每个字段。类型字符串前面的’|’、’<’、’>’等字符表示字段值的字节顺序:

  • | : 忽视字节顺序
  • < : 低位字节在前,即小端模式(little endian)
  • > : 高位字节在前,即大端模式(big endian)

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

>>> a[0]
('Zhang', 32, 75.5)
>>> a[0].dtype
dtype([('name', '|S32'), ('age', '<i4'), ('weight', '<f4')])

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

>>> 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
array([32, 24])
>>> b[0] = 40
>>> a[0]["age"]
40

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

>>> a.tofile("test.bin")

利用下面的C语言程序可以将“test.bin”文件中的数据读取出来。

02-numpy/read_struct_array.c

用C语言读取NumPy输出的结构数组文件

#include <stdio.h>

struct person
{
    char name[32];
    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);
	}
}

内存对齐

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

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

>>> 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')])

内存结构

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

/tech/static/books/scipy/_images//numpy_memory_struct.png

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

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

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

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

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

>>> b = a[::2,::2]
>>> b
array([[ 0.,  2.],
       [ 6.,  8.]], dtype=float32)
>>> b.strides
(24, 8)

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

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

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

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

  • 当下标使用整数和切片时,所取的数据在数据存储区域中是等间隔分布的。因只需要修改【图:ndarray数组对象在内存中的储存方式】所示的数据结构中的dim count、dimensions、stride等属性以及指向数据存储区域的指针data,就能实现整数和切片下标,因此新数组和原始数组能够共享数据存储区域。
  • 当使用整数序列、整数数组和布尔数组时,不能保证所取的数据在数据存储区域是等间隔的,因此无法和原始数组共享数据,只能对数据进行复制。

数组的flags属性描述了数据存储区域的一些属性,直接查看flags属性将输出各个标志的值:

>>> a.flags
C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : True
WRITEABLE : True
ALIGNED : True
UPDATEIFCOPY : False

也可以单独获得其中的某个标志值:

>>> a.flags.c_contiguous
True

几个比较重要的标志的含义如下:

  • C_CONTIGUOUS:数据存储区域是否是C语言格式的连续区域。
  • F_CONTIGUOUS:数据存储区域是否是Fortan语言格式的连续区域。
  • OWNDATA:数组是否拥有此数据存储区域,当数组是其它数组的视图时,它不拥有数据存储区域。

由于数组a是通过array()直接创建的,因此它的数据存储区域是C语言格式的连续区域,并且它拥有数据存储区域。下面我们看看数组a的转置的标志:

>>> a.T.flags
C_CONTIGUOUS : False
F_CONTIGUOUS : True
OWNDATA : False

数组的转置可以通过其T属性获得,转置数组将其数据存储区域看作是Fortan语言格式的连续区域,并且它不拥有数据存储区域。

>>> b.flags
C_CONTIGUOUS : False
F_CONTIGUOUS : False
OWNDATA : False

由于数组b是数组a的一个视图,因此它既不拥有数据存储区域,它的数据也不是连续存储的。通过视图数组的base属性可以获得保存数据的原始数组:

>>> id(b.base)
34064760
>>> id(a)
34064760

除了使用切片能够从同一块数据区创建不同的shape和strides的数组对象之外,我们还可以直接设置这些属性,从而得到用切片实现不了的效果,例如:

>>> from numpy.lib.stride_tricks import as_strided
>>> a = np.arange(6)
>>> b = as_strided(a, shape=(4, 3), strides=(4, 4))
>>> b
array([[0, 1, 2],
       [1, 2, 3],
       [2, 3, 4],
       [3, 4, 5]])
使用as_strided()时NumPy不会进行内存越界检查,因此如果shape和strides的设置不当可能会引起意想不到的错误。

这个例子中,我们从NumPy的辅助模块中载入了一个as_strided()函数。并使用它从一个长度为6的一维数组a,创建了一个shape为(4,3)的二维数组b。由于通过strides参数直接指定了数组b的strides属性,因此不仅数组b和数组a共享数据区,而且b中的前后两行有2个元素是重合的。例如下面修改a[2]的值,b中的前三行中对应的元素也发生改变:

>>> a[2] = 20
>>> b
array([[ 0,  1, 20],
       [ 1, 20,  3],
       [20,  3,  4],
       [ 3,  4,  5]])

在对数据进行处理时,可能经常需要对数据进行分块处理,而且为了保持平滑,每块数据之间需要有一定的重叠部分。这时可以使用上面介绍的方法对数据进行带重叠的分块。

內容目录

上一个主题

NumPy-快速处理数据

下一个主题

ufunc运算

本页

loading...