Numpy 学习笔记——1. 基础
概述
TODO
1 | import numpy as np |
ndarray 介绍
np.ndarray
(别名 np.array
)是 Numpy
的核心数据类型,可以用于存储 n 维数组,并提供了大量相关操作方法。
它与 Python 的 list
类型类似,但是只能存储同类型数据,并且尺寸是固定且规则的,即不允许各个元素长短不一的数组。
与 list
不同,ndarray
的运算非常高效。
ndarray
也是很多 Python
科学计算库的核心类型所依赖的基础类型。
ndarray
需要注意的一些重要属性包括:
ndarray.data
: 数组的底层数据缓冲区,通常不需要直接操作。- 尺寸信息:
ndarray.shape
: 数组的形状,包括每个维度的长度,例如(m,n)
。ndarray.ndim
: 数组的维度个数。ndarray.size
: 数组的元素总个数。
- 类型信息:
ndarray.dtype
: 数组中元素的数据类型,默认通常为float64
;ndarray.itemsize
: 数组中每个元素的大小,以字节为单位。
我们主要关注一维数组和二维数组,例如 1
2
3array([1, 2, 3]) # shape=(3,)
array([[1, 2, 3], [4, 5, 6]]) # shape=(2, 3)
一维数组的尺寸是只含一个整数的元组,而不是一个整数,或者更离谱的两个整数(MATLAB就是这么干的)。 对于二维数组,第一个维度表示行,第二个维度表示列。对于更高维的数组,Numpy 将倒数第二个维度视作行,倒数第一个维度视作列。
还值得注意的是 Numpy 数组的轴:例如一个尺寸为 (2,3,4)
的数组具有三个轴,序号依次为
0,1,2。某些操作支持指定具体的轴,例如沿着某个轴的方向取 max。
补充:
- 虽然 Numpy 在
ndarray
这个基础数据类型之外,还提供了一个特化的matrix
类型,但是它只是在早期版本(Python < 3.5)为了简化线性代数操作所进行的,例如运算符*
会表示矩阵乘法而非逐元素乘法。但是后续版本中通过@
表示矩阵乘法的效果更好,并且不存在歧义。随着版本更迭,目前官方文档中已经不推荐使用matrix
。 - Numpy
其实还支持更复杂的元素类型,但是在科学计算中,我们通常只需要考虑几种常见的即可,例如
np.float64
,np.int64
,还有布尔数组; - Numpy 的数组在内存中的排布方式支持行主序(与C语言一样)和列主序(与Fortran和MATLAB一样),并且在很多操作中支持指定,但是我们始终只考虑行主序。
创建 array
我们首先关注如何创建
ndarray
(下文主要使用别名array
)
基于字面量创建
我们可以直接通过字面量创建array
,例如通过 Python
的列表或元组,支持多层列表,也支持列表和元组混合。
1 | a = np.array([1, 2, 3]) |
注意不要直接提供多个元素,而是应该提供一个列表。 1
a = np.array(1, 2, 3) # error
在创建时可以指定类型,缺省时会自动根据字面量使用合适的类型,需要注意整数类型和浮点数类型的差异,可以使用1.0
或1.
等细节来确保使用浮点数类型。
1
2
3
4
5
6
7a1 = np.array([1, 2, 3]) # int64
a2 = np.array([1.0, 2, 3]) # float64
b = np.array([1, 2, 3], dtype=np.float64) # float64
c = np.array([1, 2, 3], dtype=complex) # complex128
特殊函数创建(一)
首先关注最常见的需求:创建一维等差数列,主要有两种方式。
np.arange
: 创建一个等差数列,需要指定起始值、结束值和步长,左闭右开区间,因此不含结束值。np.linspace
:创建一个等差数列,需要指定起始值、结束值和总数量。默认是闭区间,因此含结束值(但是可以通过endpoint=False
选项来修改)
np.arange
例如 1
2
3
4
5
6
7
8a1 = np.arange(0,10,2)
# [0 2 4 6 8]
a2 = np.arange(0,10) # 默认步长为1
# [0 1 2 3 4 5 6 7 8 9]
a3 = np.arange(10) # 默认从0开始
# [0 1 2 3 4 5 6 7 8 9]
np.linspace
例如 1
2
3
4
5
6
7
8
9a1 = np.linspace(1, 2, 11)
# [1. 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2. ]
a2 = np.linspace(1, 2, 10, endpoint=False) # 不含结束值
# [1. 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9]
a3,dx = np.linspace(1, 2, 10, endpoint=False, retstep=True) # 返回步长
# [1. 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9]
# 0.1
对于整数数组,np.range
非常实用。
但是对于浮点数数组,由于浮点误差的存在,更适合使用np.linspace
。
特殊函数创建(二)
Numpy 提供了一些函数来创建特殊的数组,例如:
np.zeros
:创建一个全零数组,可以指定尺寸和类型。np.ones
:创建一个全一数组,可以指定尺寸和类型。np.empty
:创建一个未初始化的数组,可以指定尺寸和类型。
如果传入一个数,那么会创建一个对应长度的一维数组;如果传入一个元组/列表,那么会创建一个对应尺寸的多维数组。
一维数组例如 1
2
3
4
5
6
7
8np.zeros(4)
# array([0., 0., 0., 0.])
np.ones(4)
# array([1., 1., 1., 1.])
np.empty(4)
# 随机值
多维数组例如 1
2
3
4
5
6
7
8
9
10np.zeros((2, 3))
# array([[0., 0., 0.],
# [0., 0., 0.]])
np.ones((2, 3))
# array([[1., 1., 1.],
# [1., 1., 1.]])
np.empty((2, 3))
# 随机值
这几个函数还提供了更加实用的xxx_like
版本,也就是根据传入的数组尺寸,创建同样尺寸的特殊数组。例如
1
2
3
4
5
6
7
8
9
10
11
12a = np.array([[1,2,3],[4,5,6]])
np.zeros_like(a)
# array([[0, 0, 0],
# [0, 0, 0]])
np.ones_like(a)
# array([[1, 1, 1],
# [1, 1, 1]])
np.empty_like(a)
# 随机值
特殊函数创建(三)
为了便于线性代数操作,Numpy 也提供了创建特殊二维数组的函数,功能与 MATLAB 的对应函数非常类似:
np.eye
:创建单位矩阵np.diag
:创建对角线矩阵,或者从数组中提取对角线元素
np.eye
的使用例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 传一个参数,创建单位方阵
np.eye(3)
# array([[1., 0., 0.],
# [0., 1., 0.],
# [0., 0., 1.]])
# 传两个参数,会创建指定大小的矩阵,主对角线为1,其余为0
np.eye(4, 3)
# array([[1., 0., 0.],
# [0., 1., 0.],
# [0., 0., 1.],
# [0., 0., 0.]])
# 可以指定对角线的偏移量,k>0对应右上角,k<0对应左下角
np.eye(4, k=1)
# array([[0., 1., 0., 0.],
# [0., 0., 1., 0.],
# [0., 0., 0., 1.],
# [0., 0., 0., 0.]])
np.diag
的使用例如 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# 提供一维数组作为对角线,创建对应方阵
np.diag([1, 2, 3])
# array([[1, 0, 0],
# [0, 2, 0],
# [0, 0, 3]])
# 可以指定对角线的偏移量,k>0对应右上角,k<0对应左下角
np.diag([1, 2, 3], k=1)
# array([[0, 1, 0, 0],
# [0, 0, 2, 0],
# [0, 0, 0, 3],
# [0, 0, 0, 0]])
# 提取对角线
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
np.diag(a)
# array([1, 5, 9])
# 提取指定偏移的对角线
np.diag(a, k=1)
# array([2, 6])
这两个函数的返回值仍然是
ndarray
类型。
索引
Numpy 的索引基本遵循了 Python 列表的索引规则,即从0开始,使用 -1 表示倒数第一个元素,以此类推,超过范围的索引会报错,支持切片等。 但是在此基础上,Numpy 还提供了各种复杂的索引语法。
整数索引
首先考虑最基础的索引用法,对于一维数组,读写指定位置的元素只需要提供一个整数索引即可,例如
1
2
3
4
5
6a = np.arange(10)
print(a[2]) # 2
print(a[-1]) # 9
print(a[100]) # error
对于二维数组,由于 Numpy
在逻辑上将其实现为数组的数组,因此a[i]
会得到一个一维数组,a[i][j]
则会得到一个元素。
例如 1
2
3
4a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float64)
a[1] # array([4., 5., 6.])
a[1][2] # 6.0
这里更推荐的写法是直接提供所有分量的索引:a[i,j]
,这在语法上其实相当于传递了一个元组:
1
a[i,j] == a[(i,j)]
值得注意的是,Python 内置列表并不允许这么写 1
2s = [[2, 3], [4, 5]]
s[1, 1] # error
切片
Numpy 同样支持切片操作,可以指定起点,终点(不含)和步长,例如
1
2
3
4
5
6
7
8
9
10a = np.arange(10)
a[1:10:2]
# array([1, 3, 5, 7, 9])
a[1:5]
# array([1, 2, 3, 4])
a[:] # 等价于 a[::]
# array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
考虑下面两种写法 1
2
3
4
5
6
7a = np.arange(10)
a[2:3]
# array([2])
a[2]
# np.int64(2)
虽然含义非常类似,但是注意切片返回的是一个 Numpy 数组,而整数索引返回的是一个元素。
严格来说,切片返回的不是新的数组,而是原数组的视图,这里暂不区分,视图和拷贝会在下文中进行讨论。
二维数组的切片操作需要包括每一个维度的信息,会返回一个二维数组,例如
1
2
3
4
5
6
7
8
9a = np.arange(20).reshape(4, 5)
# array([[ 0, 1, 2, 3, 4],
# [ 5, 6, 7, 8, 9],
# [10, 11, 12, 13, 14],
# [15, 16, 17, 18, 19]])
a[0:2,1:3]
# array([[1, 2],
# [6, 7]])
实际上传递的是slice
对象的元组 1
a[0:2,1:3] == a[(slice(0,2,None), slice(1,3,None))]
二维数组的切片需要包括每一个维度的信息,但是允许缺省靠后的维度,此时视作:
,也就是在这个维度全选,例如
1
2
3
4
5
6
7
8
9
10
11
12
13a = np.arange(20).reshape(4, 5)
# array([[ 0, 1, 2, 3, 4],
# [ 5, 6, 7, 8, 9],
# [10, 11, 12, 13, 14],
# [15, 16, 17, 18, 19]])
a[0:2, :]
# array([[0, 1, 2, 3, 4],
# [5, 6, 7, 8, 9]])
a[0:2]
# array([[0, 1, 2, 3, 4],
# [5, 6, 7, 8, 9]])
对于多维数组,有时可能需要连续使用多个:
(因为不是最后几个维度而无法省略),此时可以使用一个...
代替
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23a = np.arange(24).reshape(2, 3, 4)
a[:,:,1:3]
# array([[[ 1, 2],
# [ 5, 6],
# [ 9, 10]],
# [[13, 14],
# [17, 18],
# [21, 22]]])
a[...,1:3] # 等价于 a[:,:,1:3]
# array([[[ 1, 2],
# [ 5, 6],
# [ 9, 10]],
# [[13, 14],
# [17, 18],
# [21, 22]]])
a[:,1:3] # 等价于 a[:, 1:3, :]
# array([[[ 4, 5, 6, 7],
# [ 8, 9, 10, 11]],
# [[16, 17, 18, 19],
# [20, 21, 22, 23]]])
切片会返回子数组,而普通的整数索引会返回元素。
对于多维数组,我们可以将两者混合使用:只在某些维度上切片,在其它维度使用普通的整数索引。
例如 1
2
3
4
5
6
7
8a = np.arange(20).reshape(4, 5)
# array([[ 0, 1, 2, 3, 4],
# [ 5, 6, 7, 8, 9],
# [10, 11, 12, 13, 14],
# [15, 16, 17, 18, 19]])
a[0:2, 3]
# array([3, 8])
比较下面两种写法 1
2
3
4
5
6
7
8
9
10
11a = np.arange(20).reshape(4, 5)
# array([[ 0, 1, 2, 3, 4],
# [ 5, 6, 7, 8, 9],
# [10, 11, 12, 13, 14],
# [15, 16, 17, 18, 19]])
a[0,:]
# array([0, 1, 2, 3, 4])
a[:,0]
# array([ 0, 5, 10, 15])
虽然在逻辑上确实返回了第一行和第一列的结果,但是和MATLAB的行为有明显差异:
a[0,:]
返回的数组尺寸为(4,)
,而不是(1,4)
a[:,0]
返回的数组尺寸为(4,)
,而不是(4,1)
也就是说,在混合使用切片和整数索引时,Numpy 会自动将非切片的轴压缩掉。
高级索引(一)
Numpy
支持一些高级索引语法,要求输入的索引为列表、(整数或布尔类型)array
,
或者至少含有一个列表或元组或(整数或布尔类型)array
的元组。
为了便于理解,可以认为这里出现的列表和元组都被自动转换成了
array
。
由于语法可能存在冲突,要彻底排除前面讨论的基本索引:整数,slice切片,或者它们组成元组的情况。
与切片不同,高级索引操作会返回数组的副本,也就是进行拷贝。
直接提供一个整数列表,会返回对应整数索引的元素所组成的数组:
1
2
3
4
5
6
7
8
9
10
11
12a = np.arange(10)
a[[1,4,-1]]
# array([1, 4, 9])
a[np.array([1,4,-1])]
# array([1, 4, 9])
a[np.array([[1],[4],[-1]])]
# array([[1],
# [4],
# [9]])
相当于依次获取a[1],a[4],a[-1]
,然后组成新的array
数组,新数组的shape与提供的索引相同。
需要注意,这里不能使用元组,因为不同维度的索引被视作元组处理了,会导致语法报错
1
a[(1,2)] # error, == a[1,2]
对于二维数组,考虑下面的例子 1
2
3
4
5
6
7a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
a[[2, 0], [0, 1]]
# array([12, 1])
这里提供了两个尺寸一样的数组,将依次获取从两个列表中取值组成索引:(2,0)
和(0,1)
,
获取a[2,0],a[0,1]
(不是获取张量积),然后组成新的数组,新数组的尺寸与索引数组已知。
这里是非常容易被误解的,并不是类似于切片的语义:获取4个元素组成2*2的数组。
如果提供的两个维度的索引数组尺寸不一样,Numpy会尝试对其进行广播,使其扩充到一样的尺寸(也是最终结果的尺寸),例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
a[[2], [0, 1]]
# array([12, 13])
# broadcast: [2] & [0,1] -> [2,2] & [0,1]
# shape: (2,) & (2,) -> (2,)
a[2, [0,1]]
# array([12, 13])
# broadcast: 2 & [0,1] -> [2,2] & [0,1]
# shape: (2,) & (2,) -> (2,)
a[[[2]], [0,1]]
# array([[12, 13]])
# broadcast: [[2]] & [0,1] -> [[2,2]] & [[0,1]]
# shape: (1,1) & (2,) -> (1,2)
我们可以利用广播机制,实现张量积的效果 1
2
3
4
5
6
7
8
9
10a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
a[[[2], [0]], [[0, 1]]]
# array([[12, 13],
# [ 0, 1]])
# broadcast: [[2], [0]] & [[0, 1]] -> [[2,2],[0,0]] & [[0,1],[0,1]]
# shape: (2,1) & (1,2) -> (2,2)
这里需要特别注意传入的列表尺寸,Numpy
提供了一个np.ix_
函数,可以一定程度简化我们的使用,例如
1
2
3
4
5
6
7
8
9
10
11
12a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
a[[[2], [0]], [[0, 1]]]
# array([[12, 13],
# [ 0, 1]])
a[np.ix_([2, 0], [0, 1])]
# array([[12, 13],
# [ 0, 1]])
其实np.ix_
函数只是帮我们调整了一下输入的列表尺寸
1
2
3
4np.ix_([2, 0], [0, 1])
# (array([[2],
# [0]]),
# array([[0, 1]]))
如果提供的维度不足,对于缺省的维度的效果仍然是全选,例如
1
2
3
4
5
6a[[2, 0]]
# array([[12, 13, 14, 15, 16, 17],
# [ 0, 1, 2, 3, 4, 5]])
a[[2], :]
# array([[12, 13, 14, 15, 16, 17]])
对于二维和高维数组,由于不存在歧义,我们其实也可以使用元组,例如
1
2
3
4
5
6
7
8a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
a[(2,0),]
# array([[12, 13, 14, 15, 16, 17],
# [ 0, 1, 2, 3, 4, 5]])
注意这里的a[(2,0),]
和a[(2,0)]
是完全不一样的,后者等价于a[2,0]
,也就是获取对应位置的元素。
由于高级索引的语义不是张量积,如果我们确实需要获取指定的一些行和一些列组成的子矩阵,可以使用专门的np.ix_
函数
1
2
3
4
5
6
7a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
a[[2, 0], [0, 1]]
# array([12, 1])
高级索引(二)
高级索引更常见的情景是基于布尔数组的索引。
布尔数组通常来源于对数组逐个元素进行的条件判断,例如
1
2
3
4
5
6
7
8
9a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
a > 4
# array([[False, False, False, False, False, True],
# [ True, True, True, True, True, True],
# [ True, True, True, True, True, True]])
我们先考虑简单的情况,即布尔数组具有完全一样的 shape,此时的语义为
1
x[obj] == x[obj.nonzero()]
最终会返回一个一维数组(行主序),包括所有真值所对应的元素。
例如 1
2
3
4
5
6
7
8
9
10
11a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
(a > 4).nonzero()
# (array([0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2]),
# array([5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5]))
a[a > 4]
# array([ 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17])
可以发现,通过 array
的 nonzero()
方法会提取所有非零元素的行列索引,并将其拆分为单独的两个数组,然后完全遵循前面讨论的高级索引语义。
如果布尔数组的 ndim
过少(例如对列求和然后比较,就会产生与原数组尺寸不一样的布尔数组),那么仍然要保证
shape 完全匹配前几个维度,对于后续的维度相当于 1
x[obj] == x[obj.nonzero(), ...]
缺省的维度仍然是默认全选,例如 1
2
3
4
5
6
7
8
9a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
a[[False, True, False]]
# array([[ 6, 7, 8, 9, 10, 11]])
a[[True, False]] # error
当然也可以添加:
来占位指定的维度,调整布尔数组所匹配的维度,例如
1
2
3
4
5
6
7
8
9a = np.arange(18).reshape(3, 6)
# array([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11],
# [12, 13, 14, 15, 16, 17]])
a[:, [False, True, False, False, False, True]]
# array([[ 1, 5],
# [ 7, 11],
# [13, 17]])
更一般的情况,考虑布尔数组索引与其它索引的混合,那么在语义上相当于
1
x[ind_1, boolean_array, ind_2] == x[(ind_1,) + boolean_array.nonzero() + (ind_2,)]
例如 1
2
3
4
5
6
7
8
9
10
11
12a = np.arange(18).reshape(2, 3, 3)
# array([[[ 0, 1, 2],
# [ 3, 4, 5],
# [ 6, 7, 8]],
# [[ 9, 10, 11],
# [12, 13, 14],
# [15, 16, 17]]])
a[1, [True, False, True], 0:2]
# array([[ 9, 10],
# [15, 16]])
这里返回的数组的shape是(2,2)
,因为对第一个维度直接使用的是整数索引而不是切片或者高级索引。
注:Python
元组的加法是直接拼接,例如(1,) + (3,) == (1, 3)
。
高级索引和切片可以混合使用,语义通常是很自然的,例如
1 | a = np.arange(18).reshape(3, 6) |
但是需要注意的是:
- 在最终的索引元组中,一些高级索引被切片,冒号或者
np.newaxis
分隔开,例如x[arr1, :, arr2]
- 所有的高级索引连续出现,例如
x[..., arr1, arr2]
为了让某些情况下的语义更加自然,Numpy 对于这两类情况有不同的处理逻辑。
下面提供一个例子,在混合使用时,计算结果的尺寸并没有那么的显然
1
2
3
4
5
6
7
8
9
10
11
12a = np.arange(27).reshape(3, 3, 3)
a[:, [0, 1], [[2]]]
# array([[[ 2, 5]],
# [[11, 14]],
# [[20, 23]]])
# shape = (3, 1, 2)
a[[0, 1], :, [[2]]]
# array([[[ 2, 5, 8],
# [11, 14, 17]]])
# shape = (1, 2, 3)
背后的细节讨论起来比较复杂,查看官方文档中的说明即可。 在实际使用中通常并不会涉及,只需要注意这种用法存在风险即可。
广播
Numpy
提供了一种广播(broadcast)机制,用来将两个或多个不同尺寸的数组扩展为统一的尺寸,以便进行逐元素运算。
在处理涉及标量时,自动将其转换为尺寸为(1,)
的
array
数组。
以两个数组为例,广播规则具体为:
- 首先,将两个数组的维度对齐,保证两者 ndim 相同,不足的在左侧补充 1;
- 检查两个数组的尺寸元组是否满足广播条件,对于每一个轴
- 要么,两个数组关于此轴的维数相等;
- 要么,其中一个关于此轴的维数为1;
- 否则,两个数组不满足广播条件。
- 对于可广播的两个数组,对于维数不相等的轴,将维数为1的数组沿着这个维度进行复制,使得两者的维数相等。
例如:
- 对于尺寸为
(2,3)
和(3,)
的两个数组,将后者提升为(1,3)
,满足广播条件,广播得到的尺寸为(2,3)
- 对于尺寸为
(4,5)
和(2,4,2)
的两个数组,将前者提升为(1,4,5)
,检查发现第三个维度不相等,因此不满足广播条件。
Numpy 提供了 np.broadcast_shapes
函数可以检查广播的具体行为 1
2np.broadcast_shapes((2, 3), (3,)) # (2, 3)
np.broadcast_shapes((4, 5), (2, 4, 2)) # error
拷贝与视图
Python 本身就具有深拷贝和浅拷贝的概念,Numpy 对于数组提供了视图,以避免某些不必要的拷贝操作,对视图的修改会直接影响到原始数组。
使用整数或切片索引会创建视图,例如 1
2
3
4
5
6
7
8
9
10
11
12
13a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
c = a[0] # view
# array([1, 2, 3])
c[:] = 100
print(a)
# [[100 100 100]
# [ 4 5 6]
# [ 7 8 9]]
print(c)
# [[100 100 100]]
这里对c[:]
的修改会影响到原始数组。(需要注意的是,与c[:]=100
不同,c=100
是直接赋值,而不是修改数组的元素)
使用高级索引则会创建新数组,例如 1
2
3
4
5
6
7
8
9
10
11
12
13a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
c = a[[0]] # copy
# array([1, 2, 3])
c[:] = 100
print(a)
# [[1 2 3]
# [4 5 6]
# [7 8 9]]
print(c)
# [[100 100 100]]
这里对c[:]
的修改不会影响到原始数组。
需要说明的是,基于高级索引的单独赋值语句才会触发拷贝以创建新数组,如果只是赋值操作,那么仍然是就地修改
1
2
3
4
5
6
7
8a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
a[[1,2],[1,2]] = 100
print(a)
# [[ 1 2 3]
# [ 4 100 6]
# [ 7 8 100]]
判断一个对象是不是视图非常简单,检查 base
属性即可,一个真实 array
数组的 base
属性是
None
,而一个视图的 base
属性会指向它所引用的
array
数组。
例如 1
2
3
4
5
6
7
8
9
10
11
12def is_view(s):
return 'True' if s.base is not None else 'False'
a = np.array([1, 2, 3])
b = a[1:]
c = a.copy()
print(is_view(a)) # False
print(is_view(b)) # True
print(is_view(c)) # False
print(b.base is a) # True
数组变形
TODO