概述

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
3
array([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.float64np.int64,还有布尔数组;
  • Numpy 的数组在内存中的排布方式支持行主序(与C语言一样)和列主序(与Fortran和MATLAB一样),并且在很多操作中支持指定,但是我们始终只考虑行主序。

创建 array

我们首先关注如何创建 ndarray(下文主要使用别名array

基于字面量创建

我们可以直接通过字面量创建array,例如通过 Python 的列表或元组,支持多层列表,也支持列表和元组混合。

1
2
3
4
5
6
a = np.array([1, 2, 3])
# array([1, 2, 3])

b = np.array([[1, 2], (3, 4)])
# array([[1, 2],
# [3, 4]])

注意不要直接提供多个元素,而是应该提供一个列表。

1
a = np.array(1, 2, 3) # error

在创建时可以指定类型,缺省时会自动根据字面量使用合适的类型,需要注意整数类型和浮点数类型的差异,可以使用1.01.等细节来确保使用浮点数类型。

1
2
3
4
5
6
7
a1 = 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
8
a1 = 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
9
a1 = 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
8
np.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
10
np.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
12
a = 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
6
a = np.arange(10)

print(a[2]) # 2
print(a[-1]) # 9

print(a[100]) # error

对于二维数组,由于 Numpy 在逻辑上将其实现为数组的数组,因此a[i]会得到一个一维数组,a[i][j]则会得到一个元素。 例如

1
2
3
4
a = 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
2
s = [[2, 3], [4, 5]]
s[1, 1] # error

切片

Numpy 同样支持切片操作,可以指定起点,终点(不含)和步长,例如

1
2
3
4
5
6
7
8
9
10
a = 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
7
a = np.arange(10)

a[2:3]
# array([2])

a[2]
# np.int64(2)

虽然含义非常类似,但是注意切片返回的是一个 Numpy 数组,而整数索引返回的是一个元素。

严格来说,切片返回的不是新的数组,而是原数组的视图,这里暂不区分,视图和拷贝会在下文中进行讨论。

二维数组的切片操作需要包括每一个维度的信息,会返回一个二维数组,例如

1
2
3
4
5
6
7
8
9
a = 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
13
a = 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
23
a = 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
8
a = 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
11
a = 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
12
a = 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
7
a = 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
19
a = 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
10
a = 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
12
a = 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
4
np.ix_([2, 0], [0, 1])
# (array([[2],
# [0]]),
# array([[0, 1]]))

如果提供的维度不足,对于缺省的维度的效果仍然是全选,例如

1
2
3
4
5
6
a[[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
8
a = 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
7
a = 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
9
a = 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
11
a = 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])

可以发现,通过 arraynonzero() 方法会提取所有非零元素的行列索引,并将其拆分为单独的两个数组,然后完全遵循前面讨论的高级索引语义。

如果布尔数组的 ndim 过少(例如对列求和然后比较,就会产生与原数组尺寸不一样的布尔数组),那么仍然要保证 shape 完全匹配前几个维度,对于后续的维度相当于

1
x[obj] == x[obj.nonzero(), ...]

缺省的维度仍然是默认全选,例如

1
2
3
4
5
6
7
8
9
a = 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
9
a = 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
12
a = 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
2
3
4
5
6
7
8
9
10
11
12
a = 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[0:2, [0, 1]]
# array([[0, 1],
# [6, 7]])

a[0:2, 0:2]
# array([[0, 1],
# [6, 7]])

但是需要注意的是:

  • 在最终的索引元组中,一些高级索引被切片,冒号或者np.newaxis分隔开,例如x[arr1, :, arr2]
  • 所有的高级索引连续出现,例如x[..., arr1, arr2]

为了让某些情况下的语义更加自然,Numpy 对于这两类情况有不同的处理逻辑。

下面提供一个例子,在混合使用时,计算结果的尺寸并没有那么的显然

1
2
3
4
5
6
7
8
9
10
11
12
a = 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
2
np.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
13
a = 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
13
a = 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
8
a = 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
12
def 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