1.2 NumPy

前面已经安装了NumPy。现在我来介绍一下NumPy的一些基本概念和运算方法。你如果有兴趣,也可以自行查找完整的技术手册。

启动Python,尝试执行以下代码:

>>> import numpy as np
>>> np.__version__
'1.16.2'

第一行代码导入numpy模块并将其重命名为np。这种用简称来重命名模块的方式虽然不是必需的,却几乎成了通用做法。第二行代码则输出版本号,以确保安装的NumPy版本满足前面所说的最低要求。

1.2.1 定义数组

NumPy以数组为运算对象,它可以方便地将列表转换为数组。想想看,与C和Java等语言中的数组类型相比,Python中的列表类型虽然使用起来非常优雅,但是当使用列表模拟数组进行科学计算的时候,效率还是非常低的。NumPy在这方面很有优势,使用NumPy的数组类型时实际上效率很高。下面的例子首先把列表转换为数组,然后展示了一些数组属性:

>>> a = np.array([1,2,3,4])
>>> a
array([1, 2, 3, 4])
>>> a.size
4
>>> a.shape
(4,)
>>> a.dtype
dtype('int64')

上面的例子将一个包含4个元素的列表传给了np.array函数,得到一个NumPy数组。数组最基本的属性包括size和shape。这个数组的size属性为4,表示包含4个元素;shape属性则是包含4的一个元组,表示这是一个包含4个元素的一维数组或者说一维向量。如果数组是二维的,那么其shape属性将包含两个元素,分别对应每一维的大小。在下面的例子中,数组b的shape属性为(2, 4),这表示它是一个2行4列的数组。

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

1.2.2 数据类型

Python中的数据类型大体分为两种:取值几乎可以是任意大小的整型以及浮点型。NumPy数组则支持更多的数据类型。由于NumPy的底层是用C语言实现的,因此NumPy支持C语言中所有的数据类型。前面的例子向np.array函数传入了一个各元素为整数的列表,结果得到一个各元素为64位有符号整数的数组。表1-1展示了NumPy支持的数据类型。我们既可以让NumPy替我们选择数据类型,也可以显式指定数据类型。

表1-1 NumPy支持的数据类型,C语言中等价的数据类型以及取值范围

我们来看一些为数组指定类型的例子:

>>> a = np.array([1,2,3,4], dtype="uint8")
>>> a.dtype
dtype('uint8')
>>> a = np.array([1,2,3,4], dtype="int16")
>>> a = np.array([1,2,3,4], dtype="uint32")
>>> b = np.array([1,2,3,4.0])
>>> b.dtype
dtype('float64')
>>> b = np.array([1,2,3,4.0], dtype="float32")
>>> c = np.array([111,222,333,444], dtype="uint8")
>>> c
array([111, 222, 77, 188], dtype=uint8)

在上面的例子中,数组a的元素为整型,而数组b的元素为浮点型。注意在第一个关于数组b的例子中,Python自动为数组b的元素选择了64位浮点型。之所以会这样,是因为输入的列表里有一个浮点数4.0。

关于数组c的例子看起来似乎是错误的,其实不然。如果给定的数据超出指定类型的表示范围,NumPy并不会报错。在这个例子中,我们指定的8位整型只能表示[0, 255]取值范围内的整数。前面的两个数111和222属于这个范围;但后面两个数333和444都太大,NumPy默认只保留这两个数的最后8位,分别是77和188。NumPy用这个例子给我们上了一课,让我们明白了该指定什么数据类型。虽然这类问题不常出现,但我们仍须牢记于心。

1.2.3 二维数组

如果说把列表转换为数组后得到的是一维向量,那么我们可以猜测,如果把一个列表的列表转换为数组,那么得到的将是一个二维向量。事实的确如此,我们猜对了。

>>> d = np.array([[1,2,3],[4,5,6],[7,8,9]])
>>> d.shape
(3, 3)
>>> d.size
9
>>> d
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

可以看到,一个由三个子列表构成的列表被映射成了一个3 × 3的向量(即矩阵)。由于NumPy数组从0开始对元素编号,因此引用d[1,2]返回的是6。

1.2.4 全0数组和全1数组

NumPy有两个非常有用的函数:np.zeros和np.ones。它们都用于定义指定大小的数组。前者用0作为数组全部元素的初始值,而后者则将数组元素全部初始化为1。这是NumPy从头创建数组的主要方式。

>>> a = np.zeros((3,4), dtype="uint32")
>>> a[0,3] = 42
>>> a[1,1] = 66
>>> a
array([[ 0,  0, 0, 42],
       [ 0, 66, 0,  0],
       [ 0,  0, 0,  0]], dtype=uint32)
>>> b = 11*np.ones((3,1))
>>> b
array([[11.],
       [11.],
       [11.]])

这两个函数的第一个参数都是元组,用于指定各个维度的大小。如果传入标量,那么默认定义的是一个一维向量。以数组b为例,它是初始值为1、大小为3 × 1的数组,通过与标量11相乘,可以使数组b的每个元素都为11。

1.2.5 高级索引

上面的例子介绍了访问单个元素的简单索引方式。NumPy支持更复杂的索引形式,常用的一种就是用单个索引查询整个子数组,举个例子:

>>> a = np.arange(12).reshape((3,4))
>>> a
array([[ 0, 1,  2,  3],
       [ 4, 5,  6,  7],
       [ 8, 9, 10, 11]])
>>> a[1]
array([4, 5, 6, 7])
>>> a[1] = [44,55,66,77]
>>> a
array([[ 0,  1,  2,  3],
       [44, 55, 66, 77],
       [ 8,  9, 10, 11]])

这个例子用到了np.arange函数,它等价于Python中的range函数。注意这里使用reshape方法将一个大小为12的一维向量转换成了一个3 × 4的矩阵。另外请注意,a[1]返回的是整个子数组,索引是从第一维开始进行的。a[1]其实是a[1, :]的简化形示,其中的“:”表示某一维的全部元素。这种简化形式也可以用于赋值操作。

NumPy还支持Python列表的所有切片索引方式,继续上面的例子:

>>> a[:2]
array([[ 0,  1,  2,  3],
       [44, 55, 66, 77]])
>>> a[:2,:]
array([[ 0,  1,  2,  3],
       [44, 55, 66, 77]])
>>> a[:2,:3]
array([[ 0,  1,  2],
       [44, 55, 66]])
>>> b = np.arange(12)
>>> b
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>> b[::2]
array([ 0, 2, 4, 6, 8, 10])
>>> b[::3]
array([0, 3, 6, 9])
>>> b[::-1]
array([11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

首先,a[:2]返回数组a中前两行的全部元素,这里隐含了用“:”索引第二维元素,a[:2]与a[:2,:]等价。关于数组a的第三个例子对两个维度都进行了索引,通过a[:2, :3]返回了数组a的前两行和前三列元素。关于数组b的例子展示了如何每隔一个或两个元素进行查询。最后一个例子非常有用,这个例子使用一个负的增量实现了索引的倒序。当增量为−1时,表示对所有元素倒序排列。如果增量为−2,则表示以倒序每隔一个元素进行一次查询。

NumPy使用“:”来表示查询某一维的全部元素。NumPy还支持用英文省略号来表示“尽可能多的‘:’符号”。为了举例说明,下面我们先定义一个三维数组:

>>> a = np.arange(24).reshape((4,3,2))
>>> a
array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5]],
       [[ 6,  7],
        [ 8,  9],
        [10, 11]],
       [[12, 13],
        [14, 15],
        [16, 17]],
       [[18, 19],
        [20, 21],
        [22, 23]]])

可以把数组a看成4个3 × 2的子数组。如果要更新其中的第2个子数组,我们可以这么做:

>>> a[1,:,:] = [[11,22],[33,44],[55,66]]
>>> a
array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5]],
       [[11, 22],
        [33, 44],
        [55, 66]],
       [[12, 13],
        [14, 15],
        [16, 17]],
       [[18, 19],
        [20, 21],
        [22, 23]]])

这里我们显式地用“:”进行各个维度的索引,可以看到“:”的使用让NumPy很有兼容性,NumPy能够自动将列表的列表识别为数组并且执行相应的更新操作。接下来我们可以看到,用英文省略号也可以实现同样的效果。

>>> a[2,...] = [[99,99],[99,99],[99,99]]
>>> a
array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5]],
       [[11, 22],
        [33, 44],
        [55, 66]],
       [[99, 99],
        [99, 99],
        [99, 99]],
       [[18, 19],
        [20, 21],
        [22, 23]]])

这里对第3个3 × 2的子数组也进行了更新。

1.2.6 读写磁盘

NumPy数组可以通过调用np.save写到磁盘上,并通过调用np.load从磁盘上加载,比如:

>>> a = np.random.randint(0,5,(3,4))
>>> a
array([[4, 2, 1, 3],
       [4, 0, 2, 4],
       [0, 4, 3, 1]])
>>> np.save("random.npy",a)
>>> b = np.load("random.npy")
>>> b
array([[4, 2, 1, 3],
       [4, 0, 2, 4],
       [0, 4, 3, 1]])

我们先用np.random.randint创建了一个大小为3 × 4,元素取值在0和5之间(包含0和5)的随机整型数组(NumPy有很多关于随机数的函数)。然后我们将该数组写到磁盘上,命名为random.npy。数组文件必须以“.npy”为后缀,如果没有指定,NumPy会自动添加该后缀。最后,我们通过调用np.load从磁盘上加载了保存的NumPy数组。

本书还会涉及其他的NumPy函数,我会在遇到时详细介绍这些函数。接下来,我们快速了解一下SciPy。