>>> from env_helper import info; info()
页面更新时间: 2023-04-15 21:23:15
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-7-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

1.1. NumPy-快速处理数据

标准安装的Python中用列表(list)保存一组值,可以用来当作数组使用, 不过由于列表的元素可以是任何对象,因此列表中所保存的是对象的指针。 这样为了保存一个简单的 [1,2,3] ,需要有3个指针和三个整数对象。 对于数值运算来说这种结构显然比较浪费内存和CPU计算时间。

此外Python还提供了一个array模块,array对象和列表不同,它直接保存数值, 和C语言的一维数组比较类似。 但是由于它不支持多维,也没有各种运算函数,因此也不适合做数值运算。

NumPy的诞生弥补了这些不足,NumPy提供了两种基本的对象: ndarray (N-dimensional array object)和 ufunc (universal function object)。 ndarray (下文统一称之为数组)是存储单一数据类型的多维数组,而 ufunc 则是能够对数组进行处理的函数。

1.1.1. 函数库的导入与数组创新

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

>>> import numpy as np

首先需要创建数组才能对其进行其它操作。 我们可以通过给array函数传递Python的序列对象创建数组:

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

如果传递的是多层嵌套的序列,将创建多维数组(下例中的变量 c ):

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

数组的形状(Shape)

数组的大小可以通过其shape属性获得:

>>> a.shape
(4,)

数组 ashape 属性只有一个元素,因此它是一维数组。

>>> c.shape
(3, 4)

数组 cshape 属性有两个元素,因此它是二维数组,其中第0轴的长度为 3 ,第1轴的长度为 4

还可以通过修改数组的shape属性,在保持数组元素个数不变的情况下,改变数组每个轴的长度。 下面的例子将数组 cshape 改为 (4,3)

注意从 (3,4) 改为 (4,3) 并不是对数组进行转置,而只是改变每个轴的大小,数组元素在内存中的位置并没有改变:

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

在对 shape 赋值值,若某个轴的元素为 -1 时, 将根据数组元素的个数自动计算此轴的长度。

因此下面的程序将数组 cshape 变成了 (2,6) :

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

使用数组的 reshape 方法,可以创建一个改变了尺寸的新数组,原数组的shape保持不变:

>>> d = a.reshape((2,2))
>>> d
array([[1, 2],
       [3, 4]])
>>> a
array([1, 2, 3, 4])

数组 ad 其实共享数据存储内存区域, 因此修改其中任意一个数组的元素都会同时修改另外一个数组的内容:

将数组a的第一个元素改为100:

>>> a[1] = 100

注意数组d中的2也被改变了:

>>> d
array([[  1, 100],
       [  3,   4]])

数组的类型

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

>>> np.array([[1, 2, 3, 4],[4, 5, 6, 7], [7, 8, 9, 10]], dtype=float)
array([[ 1.,  2.,  3.,  4.],
       [ 4.,  5.,  6.,  7.],
       [ 7.,  8.,  9., 10.]])
>>> np.array([[1, 2, 3, 4],[4, 5, 6, 7], [7, 8, 9, 10]], dtype=complex)
array([[ 1.+0.j,  2.+0.j,  3.+0.j,  4.+0.j],
       [ 4.+0.j,  5.+0.j,  6.+0.j,  7.+0.j],
       [ 7.+0.j,  8.+0.j,  9.+0.j, 10.+0.j]])

注意上面的 dtype 参数,在较旧版本的 NumPy 中可能需要使用 np.float / np.complex 。 但在新版本中这种用法已经被弃用了。

AttributeError: module ‘numpy’ has no attribute ‘float’. np.float was a deprecated alias for the builtin float. To avoid this error in existing code, use float by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use np.float64 here. The aliases was originally deprecated in NumPy 1.20; for more details and guidance see the original release note at:

AttributeError: module ‘numpy’ has no attribute ‘complex’. np.complex was a deprecated alias for the builtin complex. To avoid this error in existing code, use complex by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, use np.complex128 here. The aliases was originally deprecated in NumPy 1.20; for more details and guidance see the original release note at:

https://numpy.org/devdocs/release/1.20.0-notes.html#deprecations

创建数组的函数

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

  • arange() 函数类似于 Python的 range()

函数,通过指定开始值、终值和步长来创建一维数组,注意数组不包括终值:

>>> 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关键字指定 是否包括终值,缺省设置是包括终值:

>>> np.linspace(0, 1, 12)
array([0.        , 0.09090909, 0.18181818, 0.27272727, 0.36363636,
       0.45454545, 0.54545455, 0.63636364, 0.72727273, 0.81818182,
       0.90909091, 1.        ])

logspace函数和linspace类似,不过它创建等比数列,下面的例子产生1(100)到100(102)、 有20个元素的等比数列:

>>> np.logspace(0, 2, 20)
array([  1.        ,   1.27427499,   1.62377674,   2.06913808,
         2.6366509 ,   3.35981829,   4.2813324 ,   5.45559478,
         6.95192796,   8.8586679 ,  11.28837892,  14.38449888,
        18.32980711,  23.35721469,  29.76351442,  37.92690191,
        48.32930239,  61.58482111,  78.47599704, 100.        ])

此外,使用frombuffer, fromstring, fromfile等函数可以从字节序列创建数组,下面以fromstring为 例:

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

>>> s = "abcdefgh"
>>> np.fromstring(s, dtype=np.int8)
/tmp/ipykernel_10874/3908796911.py:2: DeprecationWarning: The binary mode of fromstring is deprecated, as it behaves surprisingly on unicode inputs. Use frombuffer instead
  np.fromstring(s, dtype=np.int8)
array([ 97,  98,  99, 100, 101, 102, 103, 104], dtype=int8)
>>> np.frombuffer(s.encode('utf-8'), dtype=np.int8)
array([ 97,  98,  99, 100, 101, 102, 103, 104], dtype=int8)

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

np.fromstring(s, dtype=np.int16) array([25185, 25699, 26213, 26727], dtype=int16)

>>> 98*256+97
25185

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

>>> np.fromstring(s, dtype=np.float64)
/tmp/ipykernel_10874/436289145.py:1: DeprecationWarning: The binary mode of fromstring is deprecated, as it behaves surprisingly on unicode inputs. Use frombuffer instead
  np.fromstring(s, dtype=np.float64)
array([8.54088322e+194])

显然这个例子没有什么意义,但是可以想象如果我们用C语言的二进制方式写了一组double类型的数 值到某个文件中,那们可以从此文件读取相应的数据,并通过fromstring函数将其转换为float64类型 的数组。 我们可以写一个Python的函数,它将数组下标转换为数组中对应的值,然后使用此函数创建数组:

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

fromfunction函数的第一个参数为计算每个数组元素的函数,第二个参数为数组的大小(shape),因为 它支持多维数组,所以第二个参数必须是一个序列,本例中用(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.]])

1.1.2. 存取元素

数组元素的存取方法和Python的标准方法相同:

用整数作为下标可以获取数组中的某个元素。

>>> a = np.arange(10)
>>> a[5]
5

用范围作为下标获取数组的一个切片,包括a[3]不包括a[5]:

>>> a[3:5]
array([3, 4])

省略开始下标,表示从a[0]开始:

>>> a[:5]
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])

范围中的第三个参数表示步长,2表示隔一个元素取一个元素 :

>>> a[1:-1:2]
array([  1, 101,   5,   7])

省略范围的开始下标和结束下标,步长为-1,整个数组头尾颠倒:

>>> a[::-1]
array([  9,   8,   7,   6,   5,   4, 101, 100,   1,   0])

步长为负数时,开始下标必须大于结束下标:

>>> a[5:1:-2]
array([  5, 101])

和Python的列表序列不同,通过下标范围获取的新的数组是原始数组的一个视图。 它与原始数组共享同一块数据空间:

通过下标范围产生一个新的数组b,b和a共享同一块数据空间:

>>> b = a[3:7]
>>> b
array([101,   4,   5,   6])

将b的第2个元素修改为-10:

>>> b[2] = -10 #
>>> b
array([101,   4, -10,   6])

a的第5个元素也被修改为10:

>>> a
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[np.array([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])

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

>>> x = np.arange(5,0,-1)
>>> x
array([5, 4, 3, 2, 1])
>>> x[np.array([True, False, True, False, False])]
>>> # 布尔数组中下标为0,2的元素为True,因此获取x中下标为0,2的元素
array([5, 3])
>>> x[[True, False, True, False, False]]
>>> # 如果是布尔列表,则把True当作1, False当作0,按照整数序列方式获取x中的元素
array([5, 3])
>>> x[np.array([True, False, True, True, True])]
>>> # 布尔数组的长度不够时,不够的部分都当作False
array([5, 3, 2, 1])

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

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

布尔数组一般不是手工产生,而是使用布尔运算的ufunc函数产生,关于ufunc函数请参照 ufunc运算一节。

产生一个长度为10,元素值为0-1的随机数的数组:

>>> x = np.random.rand(10)
>>> x
array([0.19670979, 0.37945901, 0.11159702, 0.14366931, 0.28557334,
       0.22258492, 0.8913968 , 0.28757778, 0.58436744, 0.12703831])

数组x中的每个元素和0.5进行大小比较,得到一个布尔数组,True表示x中对应的值大于0.5:

>>> x>0.5
array([False, False, False, False, False, False,  True, False,  True,
       False])

使用x>0.5返回的布尔数组收集x中的元素,因此得到的结果是x中所有大于0.5的元素的数组:

>>> x[x>0.5]
array([0.8913968 , 0.58436744])

1.1.3. 多维数组

多维数组的存取和一维数组类似,因为多维数组有多个轴,因此它的下标需要用多个值来表示, NumPy采用组元(tuple)作为数组的下标。如图2.1所示,a为一个6x6的数组,图中用颜色区分了各个 下标以及其对应的选择区域。

1.1.4. 组元不需要圆括号

虽然我们经常在Python中用圆括号将组元括起来,但是其实组元的语法定义只需要用逗号隔开即 可,例如 x,y=y,x 就是用组元交换变量值的一个例子。

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

如何创建这个数组

你也许会对如何创建a这样的数组感到好奇,数组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)
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]])

多维数组同样也可以使用整数序列和布尔数组进行存取。

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

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

  • a[3:, [0, 2, 5]] : 下标中的第0轴是一个范围,它选取第3行之后的所有行;第1轴是整数序列,它选取第0, 2, 5三列。

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

1.1.5. 结构数组

在C语言中我们可以通过struct关键字定义结构类型,结构中的字段占据连续的内存空间,每个结构体 占用的内存大小都相同,因此可以很容易地定义结构数组。和C语言一样,在NumPy中也很容易对这 种结构数组进行操作。只要NumPy中的结构定义和C语言中的定义相同,NumPy就可以很方便地读取 C语言的结构数组的二进制数据,转换为NumPy的结构数组。 假设我们需要定义一个结构数组,它的每个元素都有name, age和weight字段。在NumPy中可以如下 定义:

>>> import numpy as np
>>> persontype = np.dtype({
>>> 'names':['name', 'age', 'weight'],
>>> 'formats':['S32','i', 'f']})
>>> 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, 指定所创建的数组的元素类 型为结构persontype。运行上面程序之后,我们可以在IPython中执行如下的语句查看数组a的元素类型

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

这里我们看到了另外一种描述结构类型的方法: 一个包含多个组元的列表,其中形如 (字段名, 类型描 述) 的组元描述了结构中的每个字段。类型描述前面为我们添加了 |',<’ 等字符,这些字符用来描述字 段值的字节顺序: 2.1. ndarray对象 21用Python做科学计算 • | : 忽视字节顺序 • < : 低位字节在前 • > : 高位字节在前 结构数组的存取方式和一般数组相同,通过下标能够取得其中的元素,注意元素的值看上去像是组 元,实际上它是一个结构:

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

dtype([(‘name’, ‘|S32’), (‘age’, ‘<i4’), (‘weight’, ‘<f4’)]) a[0]是一个结构元素,它和数组a共享内存数据,因此可以通过修改它的字段,改变原始数组中的对应 字段:

>>> c = a[1]
>>> c["name"] = "Li"
>>> a[1]["name"]
b'Li'

“Li” 结构像字典一样可以通过字符串下标获取其对应的字段值:

>>> a[0]["name"]
b'Zhang'

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

>>> b=a[:]["age"] # 或者a["age"]
>>> b
array([32, 24], dtype=int32)
>>> b[0] = 40
>>> a[0]["age"]
40

通过调用a.tostring或者a.tofile方法,可以直接输出数组a的二进制形式:

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

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

1.1.6. 内存对齐

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

#include <stdio.h>

struct person
{
    char name[32];
    int age;
    float weight;
};
struct person p[2];
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);
getchar();
}
np.dtype([('f1', [('f2', np.int16)])])

当某个字段类型为数组时,用组元的第三个参数表示,下面描述的f1字段是一个shape为(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数组对象是如何在内存中储存的。如图2.3所示,关于数组的描述信息保存 在一个数据结构中,这个结构引用两个对象:一块用于保存数据的存储区域和一个用于描述元素类型 的dtype对象。 图 2.3 - ndarray数组对象在内存中的储存方式 数据存储区域保存着数组中所有元素的二进制数据,dtype对象则知道如何将元素的二进制数据转换为 可用的值。数组的维数、大小等信息都保存在ndarray数组对象的数据结构中。图中显示的是如下数组 的内存结构:

>>> 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中的数值正好和对应轴所占据的字节数相同的话,那么数据在内存中是连续存储的。然而 数据并不一定都是连续储存的,前面介绍过通过下标范围得到新的数组是原始数组的视图,即它和原 始视图共享数据存储区域:

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

由于数组 b 和数组 a 共享数据存储区,而 b 中的第 0 轴和第 1 轴都是数组 a 中隔一个元素取一个, 因此数组 bstrides 变成了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)