NumPy 是 Python 中一个基本的科学计算库,包含以下特性:
其中,N 维数组是 NumPy 最为核心的特性。
除了显而易见的科学计算用途,NumPy 还可以用作一般数据类型的多维容器,并且是任何数据类型均可;但有一点:一个数组中必须是同种数据类型。这一特性使得 NumPy 可以高效、无缝地与各种数据库进行集成。
NumPy 基于开源许可协议 BSD license 发布,对于二次分发使用几乎没有限制。
由于 NumPy 并非 Python 的内置模块,因此我们直接从 Python 官网下载安装的发行版是不包含 NumPy 这个模块的。这个时候你要是想import numpy
,显然是会无功而返的。因此我们需要额外安装 NumPy 模块。
安装 NumPy 有好几种方式,我们这里推荐的是:1)使用pip
进行安装;2)安装Anaconda。
pip
安装这种方式推荐给已经从 Python 官网下载了某个 Python 发行版的读者,或是已经通过其它方式获得了 Python 环境,但却没有 NumPy 这个模块的读者。
安装命令:
pip install numpy
或:
python -m pip install numpy
均可。
当然,实际上 NumPy 模块本身也有很多依赖,也需要其他一些模块才能够真正发挥出它强大的功能,因此我们推荐一次安装多个模块:
python -m pip install --user numpy scipy matplotlib ipython jupyter pandas sympy nose
这种方式适合还没有安装 Python 的读者,或是已经安装了 Python 但是想一劳永逸拥有大多数科学计算库的读者。
访问 Anaconda 官网找到下载链接进行安装即可。
或者如果你觉得 Anaconda 过于臃肿,也可以安装其简化版本 Miniconda 。
N 维数组,也称“多维数组”,这里的“N”代表的就是数组的维数。
我们在之前已经学过了 Python 中的内置数据类型,其中有一种在表现形式上跟多维数组很像——就是序列(sequence)型的列表(list)数据:二者都是由中括号括起来的数据类型。
但实际上,多维数组和列表是完全不同的两种数据类型,我们在使用的时候一定要严格地将二者区分开来。具体地讲,二者有以下不同点:
本小节代码示例来自《何为 NumPy》
让我们来看一个例子(暂时忽略对变量的定义,假定存在 a、b、result 这三个数组)。
假设我们现在有 a、b 两个一维数组(类似于一维列表),要对它们进行“对应元素相乘”的运算,如果使用 Python 内置的列表,应该写成什么样呢?
result = []for i in range(len(a)): result.append(a[i]*b[i])
显然结果是正确无误的。但是我们考虑一个情形:如果 a、b 两个一维数组中每个都包含数百万甚至更多的元素,会发生什么呢?众所周知,Python 作为一门解释型语言,其代码的执行速度本就远低于众多的编译型语言,并且之前我们也讲到过,Python 中的for
循环实际上是一个迭代计算的过程,这样就会导致性能上成本很高;大多数时间都被浪费在了对代码的解释和对对象的操作上。
讲到性能问题,我们一下就想到了日常被拉出来和 Python 比较的 C 语言。那么我们用 C 语言来实现上面这个运算又如何呢?
for (i = 0; i < rows; i++): { result[i] = a[i]*b[i];}
较之之前的 Python 代码,显然这段 C 代码节省了解释代码和操作对象的性能开销,但凡事有利有弊,提升了性能的同时也牺牲了 Python 代码的简洁明了。如果是二维数组呢?
for (i = 0; i < rows; i++): { for (j = 0; j < columns; j++): { result[i][j] = a[i][j]*b[i][j]; }}
好了,不用再往上加了。我们完全可以想见,随着数组维数越来越多,这段代码嵌套的循环也将越来越多,代码越来越庞杂——总之直观性越来越差。
如果用上 NumPy 呢?同样是计算两个数组的逐元素乘积,NumPy 对于包含多维数组的运算默认模式就是“逐元素运算”,同时又通过预编译好的 C 代码来实现高性能的计算。使用示例如下:
result = a * b
啊哈!是不是有点眼熟?这不就是一个很自然的数学表达式吗?当然跟矩阵的点乘还是不一样的,但是只要了解 NumPy 的这个运算规则的人,看到这行代码就有一种一目了然之感。管你三七二十,两维、十维还是八十、八百维,我都用这一个表达式全部搞定。既保留了 Python 代码简洁明了的风格,也获得了较高的执行速度。
在上面的例子中,支撑起 NumPy 强大功能的主要有两个很重要的特性:矢量化(vectorization)和广播(broadcasting)。
其中,矢量化描述的是这么个情形:凡是在遇到需要显式循环、索引的地方,都可以省略掉这些细枝末节,由预编译好的、优化过的 C 代码在后台默默耕耘。这么一来就带来了几个好处:
for
循环。“广播”这个特性对于掌握 NumPy 非常重要,文章后面会单独讲解一下,希望读者能够用心理解
“广播”是一个 NumPy 的术语,描述的则是这么一个情况:NumPy 中的运算都是默认逐元素进行的。这些运算不仅限于常见的算术运算,也包括不那么常见的逻辑运算、位运算,以及函数,等等,凡是用到这些运算,或者说操作,NumPy 都默认是逐元素进行的;这就叫“广播”。
从前面的例子来看,进行逐元素乘法的 a、b 两个数组,既可以是大小相同的多维数组,也可以其中一个是标量、另一个是数组,甚至可以是两个大小不同的数组,为了使得“广播”的语意明确、结果清晰,其中较小的那个数组就可以被扩展为较大数组的大小。
本小节代码来自《NumPy 快速入门》
创建数组(array)的方式有不少,其中最自然的一种方式就是通过列表来生成数组。
要使用 NumPy 模块,首先我们要在当前的 Python 环境中导入 NumPy,同时为了便于之后的引用,我们将其重命名为np
:
>>> import numpy as np
文章前面我们提到过,NumPy 的多维数组和 Python 内置的列表长得很像,估计是表亲还是啥的。并且嵌套的列表在一定程度上也能够实现多维数组的功能,因此 NumPy 也很人性化的提供了接口,可以将现成的列表转换为我们要的多维数组。
>>> first_array = np.array([2,3,4])>>> first_arrayarray([2, 3, 4])>>> first_array.dtypedtype('int32')
其中,多维数组的dtype
属性指明了多维数组中元素的类型。当然也可以将列表变量作为参数:
>>> example_list = [2.0,3.0,4.0]>>> second_array = np.array(example_list)>>> second_arrayarray([2., 3., 4.])>>> second_array.dtypedtype('float64')
要注意的是,通过这种方式创建数组,经常犯的一个错误是缺少了列表的方括号,这样参数就不再是一个列表,而是好几个独立的参数了:
>>> a = np.array(1,2,3,4) # 这是错的>>> a = np.array([1,2,3,4]) # 这样才是对的
实际上不仅是列表,同为 Python 中的序列类型,列表的亲兄弟元组也可以起到相同的作用:
>>> a = np.array((2,3,4))>>> aarray([2, 3, 4])
同时,使用array
创建数组时,如果提供的序列对象是嵌套的,NumPy 还可以直接据此生成二维、三维甚至更高维的多维数组:
>>> a = np.array(((2,3,4,5),(4,5,6,7)))>>> aarray([[2, 3, 4, 5], [4, 5, 6, 7]])>>> >>> a = np.array([(2,3,4,5),(4,5,6,7)])>>> aarray([[2, 3, 4, 5], [4, 5, 6, 7]])
还能在创建数组的同时显式指定数据类型:
>>> complex_array = np.array([[1,2],[3,4]], dtype='complex')>>> complex_arrayarray([[1.+0.j, 2.+0.j], [3.+0.j, 4.+0.j]])>>> >>> float_array = np.array([[1,2],[3,4]], dtype='float64')>>> float_arrayarray([[1., 2.], [3., 4.]])
一般来说,很多时候我们都是知道多维数组的大小,但不知道其元素具体的值,因此 NumPy 提供了一些函数,可以创建以占位符初始化的固定大小的多维数组。其中,zeros
创建的是全为 0 的多维数组,ones
创建的时候全为 1 的多维数组,而empty
创建的则是随机初始值的多维数组,数组大小由一个序列(即列表或元组,建议使用元组)参数给定。并且这几种方式的默认类型都是flloat64
。
>>> np.zeros((3,4))array([[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]])>>> >>> np.ones((3,4))array([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]])>>>>>> np.empty((3,4)) # empty 的结果就是一块没有初始化的内存。这里由于形状相同,是直接取了上一个数组的值array([[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]])>>> >>> np.empty((3,5)) # 增大数组的大小就可以得到预期的随机结果了array([[1.40546330e-311, 1.40546113e-311, 1.37961302e-306, 6.23053614e-307, 8.45593934e-307], [7.56593017e-307, 8.01097889e-307, 1.78020169e-306, 7.56601165e-307, 1.02359984e-306], [2.04719290e-306, 1.00132653e-307, 1.78021527e-306, 1.66889876e-307, 3.49694131e-317]])
NumPy 还提供了一个类似于 Python 内置的range
的函数arange
,用以创建一个由等差序列组成的数组:
>>> np.arange(10,30,5)array([10, 15, 20, 25])>>> >>> np.arange( 0, 2, 0.3 )array([ 0. , 0.3, 0.6, 0.9, 1.2, 1.5, 1.8])>>> >>> np.arange(0.2,0.3,0.01)array([0.2 , 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29])
可以看到,arange
的参数还可以是浮点数,与range
略有不同。其他的特性都差不多,不再赘述。
由于浮点精度的原因,使用arange
创建浮点数组时,我们不能保证得到我们预期大小的数组,因此这个时候就建议使用linspace
这个函数。linspace
与arange
的区别就在于它们的第三个参数:前者指定的是最终得到的元素个数,而后者指定的则是元素间的步长。
>>> np.linspace(0.2,0.3,9)array([0.2 , 0.2125, 0.225 , 0.2375, 0.25 , 0.2625, 0.275 , 0.2875, 0.3 ])
多维数组是 NumPy 中最主要的对象。其实就是一个由同种元素组成的元素表,可以由元组进行索引。在 NumPy 中,维度又被称作“轴(axe)”。
注意,在继续介绍数组的各种属性之前,我们要区别开“数组的维度”和“数组某个轴上的维度”。
“数组的维度”指的是数组的“轴”数,用维度空间的概念来理解,也就是数组能够在多少个方向上具有坐标。比如一维的线性空间中,数组就只能在 x 方向上具有坐标;对于二维的平面空间,数组就在 x 和 y 两个方向上具有坐标。
而“数组轴的维度”则是指的在某个特定的方向上,数组可以有几个刻度,或者说“层次”。比如一维数组[0,1,2]
就是在 x 方向上具有 3 个层次;二维数组[[0,1,2],[2,3,4]]
则是在 x 方向上具有 2 个层次,每个层次都是一个在 y 方向上的三个层次的一维数组,在 y 方向上具有 3 个层次,每个层次都是一个在 x 方向上具有两个层次的一维数组。
下面介绍 NumPy 多维数组的基本属性:
ndim
即“n dimension”的简写。该属性指示的是多维数组的维数,或者说是“轴数”。
字面意思。这个属性指示的是多维数组整体的维度,或者说是多维数组的“形状”。是一个整型元组,每一个元素都对应与相应轴上的维数。对 n 行 m 列的矩阵而言,它的
shape
就是(n,m)
。shape
的元素个数等于多维数组的轴数。
多维数组中元素的总个数。等于
shape
中各元素之积。
dtype
实际上是“data type”的简写,意味着它指示的是多维数组中元素的数据类型。
多维数组中,每个元素的字节大小。等效于
dtype.itemsize
。
指示的是包含多维数组中元素实际内存的缓冲区。通常用不到。
数组在打印的时候长得跟嵌套列表差不多,但其排布都要遵循以下规律:
也就是说,一维数组按行打印,二维数组按矩阵形式打印,三维及更高维数组会打印成矩阵列表。
>>> print(np.zeros((2,))) # 一维数组[0. 0.]>>>>>> print(np.zeros((2,3))) # 二维数组[[0. 0. 0.] [0. 0. 0.]]>>>>>> print(np.zeros((2,3,4))) # 三维数组[[[0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.]] [[0. 0. 0. 0.] [0. 0. 0. 0.] [0. 0. 0. 0.]]]>>> >>> print(np.zeros((2,3,4,5))) # 四维数组[[[[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]] [[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]] [[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]]] [[[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]] [[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]] [[0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.] [0. 0. 0. 0. 0.]]]]
另外,为了更灵活地使用多维数组,NumPy 还提供了reshape
方法,可以将多维数组重整为某个大小。
比如在图像识别领域,就需要图像作为机器学习输入数据,而实用的机器学习应用图像来源又是不确定的,因此图像的像素阵列大小不一定一致。
使用reshape
要求参数乘积与被重整的数组元素个数相同:
>>> np.arange(12)array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])>>> >>> np.arange(12).reshape(2,6)array([[ 0, 1, 2, 3, 4, 5], [ 6, 7, 8, 9, 10, 11]])>>> >>> np.arange(12).reshape(6,2)array([[ 0, 1], [ 2, 3], [ 4, 5], [ 6, 7], [ 8, 9], [10, 11]])
并且reshape
还允许缺省 1 个参数(用-1
占位),它会根据数组元素的总数和提供的其他参数自动求出一个合适的值,从而得到新的大小的数组:
>>> np.arange(12).reshape(2,3,2)array([[[ 0, 1], [ 2, 3], [ 4, 5]], [[ 6, 7], [ 8, 9], [10, 11]]])>>> >>> np.arange(12).reshape(2,-1,2)array([[[ 0, 1], [ 2, 3], [ 4, 5]], [[ 6, 7], [ 8, 9], [10, 11]]])
本节参考自《NumPy 索引》。示例代码多来自《NumPy 索引》。
所谓索引,在 NumPy 中指的是任何用中括号来获取数组元素值的行为。
NumPy 中,索引的方式有很多,这既使得 NumPy 更加强大灵活,也带来了难于辨析的问题。
最简单的一种索引方式就是单个索引。对于一维数组,我们可以像对 Python 中的序列一样进行索引:
>>> x = np.arange(10)>>> x[2]2>>> x[-2]8
对于二维和更高维的数组,我们可以在同一个中括号内直接索引:
>>> x.shape = (2,5) # 等同于 x = x.reshape(2,5)>>> x[1,3]8>>> x[1,-1]9
而不必像对嵌套序列一样,用多个中括号分别索引:
>>> y = [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]] # 这是一个列表,不是 NumPy 多维数组!>>> y[[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]]>>> y[1][3]8
当给出的索引少于数组维数(轴数)时,得到的会是一个数组对象:
>>> x[0]array([0, 1, 2, 3, 4])
而对于返回的这个数组,我们又可以继续索引,因此对于多维数组而言,也可以使用嵌套序列的索引方式,即使用多个中括号(但这种方式比一次索引要更低效,因此不推荐):
>>> x[0][2]2
此外,NumPy 多维数组又可以像列表一样,进行切片(用冒号“:”):
>>> x = np.arange(10)>>> xarray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])>>> x[2:5]array([2, 3, 4])>>> x[1:7:2]array([1, 3, 5])
数组也可以用另一个数组来索引:
>>> x = np.arange(10,1,-1)>>> xarray([10, 9, 8, 7, 6, 5, 4, 3, 2])>>> x[np.array([3, 3, 1, 8])]array([7, 7, 9, 2])
也可以用多个数组来进行索引:
>>> y = np.arange(35).reshape(5,7)>>> yarray([[ 0, 1, 2, 3, 4, 5, 6], [ 7, 8, 9, 10, 11, 12, 13], [14, 15, 16, 17, 18, 19, 20], [21, 22, 23, 24, 25, 26, 27], [28, 29, 30, 31, 32, 33, 34]])>>> y[np.array([0,2,4]), np.array([0,1,2])]array([ 0, 15, 30])>>> >>> y[np.array([0,2,4]), 1]array([ 1, 15, 29])
还可以用布尔数组作为掩码,筛选数组元素:
>>> b = y>20>>> barray([[False, False, False, False, False, False, False], [False, False, False, False, False, False, False], [False, False, False, False, False, False, False], [ True, True, True, True, True, True, True], [ True, True, True, True, True, True, True]])>>> y[b]array([21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34])
对应于布尔数组中为“真”的元素就被筛选出来了。
本小节参考自《NumPy 广播》和《NumPy 广播机制详解》。图片来自《NumPy 广播机制详解》。
NumPy 中,各种运算默认都是“逐元素”进行的。最简单的例子就是两个明显大小相同的数组运算:
>>> a = np.array([1.0, 2.0, 3.0])>>> b = np.array([2.0, 2.0, 2.0])>>> a * barray([ 2., 4., 6.])
但是逐元素计算有一个问题:对于形状不太像的数组怎么办呢?比如下面这个数组和标量的乘法运算:
>>> multi_array = np.array([1.0,2.0,3.0])>>> scalar = 2.0>>> multi_array * scalar # array([2., 4., 6.])
按“逐元素”运算的预期,显然我们是期望把这个标量与数组的每一个元素相乘,来得到一个新的数组。但是 NumPy 懂得这其中的逻辑吗?诶?好像还真的懂。
既然我们号称 NumPy 可以用自然的方式来表达数学公式,肯定不能把一个简单的标量乘向量弄得太复杂,同样是直接乘就可以了,得到的结果与之前两个大小相同的数组直接相乘是一样的:
>>> multi_array * scalararray([2., 4., 6.])
在这里边,标量scalar
就好像被扩展为了一个跟multi_array
大小相当的数组一样。
在 NumPy 的广播机制中,有一个很重要的概念叫做“相容的形状”。只有当两个数组具有“相容的形状”时,“广播”才能起作用;否则抛出异常ValueError: operands could not be broadcast together with shapes xx yy
。
所谓“相容的形状”,指的是参与运算的这两个数组各个维度要么 1)相等;要么 2)其中一个数组的对应维度为 1(不存在的维度也是 1)。
而 NumPy 比较各个维度的顺序是从后往前,一次比较,就相当于把参与运算的数组形状右对齐,然后若相等就再往前看,若其中一个为 1 就将其在这个维度上扩展到更高的维度,直到第一个维度。
下面是对于上述规则一个更清晰的表述描述:
Image (3d array): 256 x 256 x 3Scale (1d array): 3Result (3d array): 256 x 256 x 3A (4d array): 8 x 1 x 6 x 1B (3d array): 7 x 1 x 5Result (4d array): 8 x 7 x 6 x 5
但是像这样的两个数组就无法通过“广播”实现逐元素运算了:
A (1d array): 3B (1d array): 4 # 最后一个维度无法匹配A (2d array): 2 x 1B (3d array): 8 x 4 x 3 # 倒数第二个维度无法匹配
对于一个多维数组和一个一维数组的运算,实例如下:
>>> a = np.array([[ 0.0, 0.0, 0.0],... [10.0, 10.0, 10.0],... [20.0, 20.0, 20.0],... [30.0, 30.0, 30.0]])>>> b = np.array([1.0, 2.0, 3.0])>>> a.shape(4, 3)>>> b.shape(3,)>>> a + barray([[ 1., 2., 3.], [11., 12., 13.], [21., 22., 23.], [31., 32., 33.]])
广播图示如下:
NumPy 向量广播图示对齐之后,维度不相容是万万不行的:
>>> a = np.array([[ 0.0, 0.0, 0.0],... [10.0, 10.0, 10.0],... [20.0, 20.0, 20.0],... [30.0, 30.0, 30.0]])>>> b = np.array([1.0, 2.0, 3.0])>>> a.shape(4, 3)>>> b.shape(4,)>>> a + bTraceback (most recent call last): File "", line 1, in ValueError: operands could not be broadcast together with shapes (4,3) (4,)
图示如下:
NumPy 向量无法广播的图示当然我们也可以通过reshape
重整b
数组的形状,以适应广播的要求:
>>> b.shape(4,)>>> b = b.reshape(4,1)>>> b.shape(4, 1)>>> barray([[1.], [2.], [3.], [4.]])>>> a + barray([[ 1., 1., 1.], [12., 12., 12.], [23., 23., 23.], [34., 34., 34.]])
这样就没问题了。
看了那么多枯燥的原理,我们接下来轻松一下,看看 NumPy 还能干什么。
喜欢拍照的同学都知道,图片是由“像素(pixel)”构成的。所谓“像素”,英文 pixel 就是“picture element”的简写,指的是“构成图像的元素”。
实际上我们可以把一张图片纵横切分成很多小块,这些最基本的小方格就是构成多姿多彩的数字图像世界的一砖一瓦。作为二维的图像,它们像素的排布是不是跟二维数组很像?诶~ 对了,我们可以用一个很常用的图形库matplotlib
来读取图像,得到的实际上就是一个 NumPy 二维数组:
>>> import matplotlib.pyplot as plt>>> image = plt.imread("python-logo.png")>>> image.shape(600, 800, 3)
Python Logo
取下一半图像:
>>> image_crop = image[300:,::,::]>>> plt.imshow(image_crop)>>> plt.show()
Python Logo 裁剪图
取右边一半的图像:
>>> image_crop = image[:,400:,:]>>> plt.imshow(image_crop)>>> plt.show()
Python Logo 裁剪图
本文对我们以后经常会用到的 NumPy 模块进行了简要的介绍。虽然本文篇幅已经很长,但对于 NumPy 相关知识的讲解依然只是沧海一粟。
本文的目的在于给读者提供一个粗略的印象,记不住没关系,希望在以后的使用中读者能够熟练掌握 NumPy 的使用。
[1]https://numpy.org/
[2]https://numpy.org/devdocs/user/whatisnumpy.html
[3]https://numpy.org/devdocs/user/quickstart.html
[4]https://numpy.org/devdocs/user/basics.indexing.html
[5]https://numpy.org/devdocs/user/basics.broadcasting.html
[6]https://numpy.org/devdocs/user/theory.broadcasting.html
系列文章 第78天:Python 操作 MongoDB 数据库介绍 第77天:Python 操作 SQLite 第76天:Python Scrapy 模拟登陆第75天:Python 操作 Redis 数据库介绍 第74天:Python newspaper 框架 第73天:itchat 微信机器人简介 第72天:PySpider框架的使用第71天:Python Scrapy 项目实战 从 0 学习 Python 0 - 70 大合集总结 PS: 公号内回复 :Python,即可进入Python 新手学习交流群,一起 100天计划! -END- Python 技术 关于 Python 都在这里示例代码:https://github.com/JustDoPython/python-100-day