标准安装的Python中用列表(list)保存一组值,可以用来当作数组使用,
不过由于列表的元素可以是任何对象,因此列表中所保存的是对象的指针。
这样为了保存一个简单的 [1,2,3]
,需要有3个指针和三个整数对象。
对于数值运算来说这种结构显然比较浪费内存和CPU计算时间。
此外Python还提供了一个 array
模块,array
对象和列表不同,它直接保存数值,
和C语言的一维数组比较类似。但是由于它不支持多维,也没有各种运算函数,因此也不适合做数值运算。
NumPy的诞生弥补了这些不足,NumPy提供了两种基本的对象:
ndarray
(N-dimensional array object)和 ufunc
(universal function object)。
ndarray
(下文统一称之为数组)是存储单一数据类型的多维数组,而 ufunc
则是能够对数组进行处理的函数。
import numpy
numpy.__version__
'2.1.3'
习惯上,大多数人都会使用 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')
数组的形状(Shape)
数组的大小可以通过其 shape
属性获得:
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]])
数组的类型
数组的元素类型可以通过 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 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(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_140/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)
/tmp/ipykernel_140/1433481606.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.int16)
array([25185, 25699, 26213, 26727], dtype=int16)
98*256+97
25185
如果把整个字符串转换为一个64位的双精度浮点数数组,那么它的值是:
np.fromstring(s, dtype=np.float64)
/tmp/ipykernel_140/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])
x = np.arange(10,1,-1)
x
array([10, 9, 8, 7, 6, 5, 4, 3, 2])
获取x中的下标为3, 3, 1, 8的4个元素,组成一个新的数组。
x[[3, 3, 1, 8]]
array([7, 7, 9, 2])
b = x[np.array([3,3,-3,8])]
下标可以是负数
b[2] = 100
b
array([ 7, 7, 100, 2])
由于b和x不共享数据空间,因此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(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([5, 3])
布尔数组的长度不够时,不够的部分都当作 False
x[np.array([True, False, True, True, True])]
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.5034948 , 0.75337918, 0.00762814, 0.31412491, 0.69908191, 0.3908021 , 0.33044917, 0.25918241, 0.9391951 , 0.25003541])
数组x中的每个元素和0.5进行大小比较,得到一个布尔数组, True
表示x中对应的值大于0.5:
x>0.5
array([ True, True, False, False, True, False, False, False, True, False])
使用 x>0.5
返回的布尔数组收集x中的元素,因此得到的结果是 x
中所有大于 0.5
的元素的数组:
x[x>0.5]
array([0.5034948 , 0.75337918, 0.69908191, 0.9391951 ])
组元不需要圆括号
虽然我们经常在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列。
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')])
这里我们看到了另外一种描述结构类型的方法: 一个包含多个组元的列表,
其中形如 (字段名, 类型描述) 的组元描述了结构中的每个字段。
类型描述前面为我们添加了 |
, >
, <
等字符,这些字符用来描述字段值的字节顺序:
|
: 忽视字节顺序<
: 低位字节在前>
: 高位字节在前
结构数组的存取方式和一般数组相同,通过下标能够取得其中的元素, 注意元素的值看上去像是组元,实际上它是一个结构:
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')])
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'']
:
或者 a["age"]
b=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')])
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)