标准安装的Python中用列表(list)保存一组值,可以用来当作数组使用,
不过由于列表的元素可以是任何对象,因此列表中所保存的是对象的指针。
这样为了保存一个简单的 [1,2,3]
,需要有3个指针和三个整数对象。
对于数值运算来说这种结构显然比较浪费内存和CPU计算时间。
此外Python还提供了一个array模块,array对象和列表不同,它直接保存数值, 和C语言的一维数组比较类似。 但是由于它不支持多维,也没有各种运算函数,因此也不适合做数值运算。
NumPy的诞生弥补了这些不足,NumPy提供了两种基本的对象: ndarray
(N-dimensional array object)和 ufunc
(universal function object)。
ndarray
(下文统一称之为数组)是存储单一数据类型的多维数组,而 ufunc
则是能够对数组进行处理的函数。
完成安装 NumPy 之后,你就可以在你的脚本中载入NumPy模块了,然后输出NumPy的版本号验证安装结果:
import numpy
numpy.__version__
'2.0.2'
习惯上,大多数人都会使用 np
作为别名来载入NumPy模块:
import numpy as np
在这里介绍的内容会遵循上面的习惯用法。
在你阅读本章的过程中,请不要忘记了IPython提供的內建帮助工具?
以及使用制表符自动补全的功能。
例如,要查看numpy模块中的所有内容(属性和方法),你可以输入:
In [3]: np.<TAB>
如果想查看numpy的內建文档,你可以输入:
In [4]: 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')
a.shape
(4,)
数组 a
的 shape
属性只有一个元素,因此它是一维数组。
c.shape
(3, 4)
数组 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]])
在对 shape
赋值值,若某个轴的元素为 -1
时,
将根据数组元素的个数自动计算此轴的长度。
因此下面的程序将数组 c
的 shape
变成了 (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])
数组 a
和 d
其实共享数据存储内存区域,
因此修改其中任意一个数组的元素都会同时修改另外一个数组的内容:
将数组a的第一个元素改为100:
a[1] = 100
注意数组d中的2也被改变了:
d
array([[ 1, 100], [ 3, 4]])
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 builtinfloat
. To avoid this error in existing code, usefloat
by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, usenp.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 builtincomplex
. To avoid this error in existing code, usecomplex
by itself. Doing this will not modify any behavior and is safe. If you specifically wanted the numpy scalar type, usenp.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
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(10^0)到100(10^2)、 有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_133/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_133/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.]])
a = np.arange(10)
a[5]
np.int64(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.24934878, 0.16055198, 0.87873077, 0.11291854, 0.10744736, 0.5309738 , 0.01262217, 0.77380487, 0.33459603, 0.5864481 ])
数组x中的每个元素和0.5进行大小比较,得到一个布尔数组,True表示x中对应的值大于0.5:
x>0.5
array([False, False, True, False, False, True, False, True, False, True])
使用x>0.5返回的布尔数组收集x中的元素,因此得到的结果是x中所有大于0.5的元素的数组:
x[x>0.5]
array([0.87873077, 0.5309738 , 0.77380487, 0.5864481 ])
图 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 列。
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]
np.void((b'Zhang', 32, 75.5), dtype=[('name', 'S32'), ('age', '<i4'), ('weight', '<f4')])
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"]
np.bytes_(b'Li')
"Li" 结构像字典一样可以通过字符串下标获取其对应的字段值:
a[0]["name"]
np.bytes_(b'Zhang')
我们不但可以获得结构元素的某个字段,还可以直接获得结构数组的字段,它返回的是原始数组的视 图,因此可以通过修改b[0]改变a[0][''age'']:
b=a[:]["age"] # 或者a["age"]
b
array([32, 24], dtype=int32)
b[0] = 40
a[0]["age"]
np.int32(40)
通过调用a.tostring或者a.tofile方法,可以直接输出数组a的二进制形式:
a.tofile("test.bin")
利用下面的C语言程序可以将test.bin文件中的数据读取出来。
#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
中隔一个元素取一个,
因此数组 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)