本文运行环境: Python 3.6.9 64-bit, WSL: Ubuntu-18.04
一、 前言
Python中, numpy提供了强大的科学计算库, 其中ndarray
是其中表示数组的类, 其全称为The N-dimensional array
(N维数组)
这篇文章不够全面、仅供入门, 希望学习完这篇后能够在实践中逐渐上手numpy
这里推荐Github中的图像处理100问(中文版), 其中大量用到了ndarray
, 参考答案中的代码也写得十分优雅, 感兴趣的看官不妨稍候移步于此
二、 正篇
2.1 限定条件
ndarray
比list
/tuple
强大得多, 但对其中的数据有更严格的限定: 是存储着相同类型和大小的元素的多维数组
对于list
/tuple
, 可以有不同数据类型混搭的情况, 如:(1, "hello")
但对于ndarray
, 每一个维度之间不止类型相同、大小也要一致(在其他语言中”数组”都是这样):
1
2
[1, "hello", 1.56] #类型不相同
[[1, 2, 3], [4, 5]] #类型相同但大小不一致
当我们试图在第二种情况下创建ndarray
时, 创建仍会成功, 不过会有如下警告:
1
2
VisibleDeprecationWarning:
Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray
可以看到, 大小不一致的两个列表被当做对象, 强行创建ndarray
了
1
[list([1, 2, 3]) list([7])]
ndarray
这样的限定条件, 决定了其对矩阵(二维数组)的良好支持
2.2 创建ndarray
最常见的方法是通过已有的list
/tuple
创建, 可以指定数据类型dtype
1
2
3
4
5
6
7
a = [[1,2,3], [4,5,6]]
a_ndy = np.array(a, dtype=complex)
print(a_ndy)
# 输出:
# [[1.+0.j 2.+0.j 3.+0.j]
# [4.+0.j 5.+0.j 6.+0.j]]
创建特殊的ndarray
- 元素全为0的数组: zeros(shape) 参数是表示形状的元组
- 元素全为1的数组: ones(shape) 参数是表示形状的元组
- 元素随机(float)的数组: empty(shape) 参数是表示形状的元组
- 单位矩阵: identity(size) 参数是方阵的大小
- 对角线为1, 其余为0的矩阵: eye(row, col, offset), 参数是行数、列数、偏移量(决定对角线位置)
1
2
3
4
5
6
7
8
9
10
11
12
>>> np.eye(3,3,0)
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
>>> np.eye(3,3,1)
array([[0., 1., 0.],
[0., 0., 1.],
[0., 0., 0.]])
>>> np.eye(3,3,-2)
array([[0., 0., 0.],
[0., 0., 0.],
[1., 0., 0.]])
与已有数组形状(shape)相同的全0/全1数组: zeros_like/ones_like(arr, dtype=None), 参数是已有数组与数据类型
等差数列:
- arangge(min, max, step): 通过最值和步长创建, 前闭后开, 当存在一个参数为
float
时dtype=float
- linspace(min, max, count): 通过最值和元素个数创建, 闭区间,
dtype=float
(无论是否整除)
- arangge(min, max, step): 通过最值和步长创建, 前闭后开, 当存在一个参数为
1
2
3
4
b = np.arange(1, 10, 3); print(b) # [1 4 7]
c = np.arange(1, 10, 3.0); print(c) # [1. 4. 7.]
d = np.arange(1, 10.1, 3); print(d) # [ 1. 4. 7. 10.]
e = np.linspace(0, 10, 6); print(e) # [ 0. 2. 4. 6. 8. 10.]
- 等比数列:
- logspace(min, max, count, [base=10]): 根据前三个参数创建等差数列, 将获得的等差数列作为
base
的指数, 这样就得到了等比数列, 缺省状态下的base
为10, 可以通过指定来改变底数
- logspace(min, max, count, [base=10]): 根据前三个参数创建等差数列, 将获得的等差数列作为
1
2
3
4
f = np.logspace(1, 10, 10, base=2); print(f)
# 输出:
# [ 2. 4. 8. 16. 32. 64. 128. 256. 512. 1024.]
2.3 获取ndarray
的属性
通过.属性
的形式获取ndarray
对象的属性
描述 | 属性 |
---|---|
ndim | 维数 |
shape | 形状(各个维的长度) |
size | 全部元素的数量 |
dtype | 包含元素的数据类型 |
itemsize | 包含元素的大小 |
real/imag | 元素的实部和虚部 |
2.4 ndarray
的索引、切片
普通的索引既可以想C语言那样将不同维度分开, 也可以合并起来
1
2
a_ndy[0][1]
a_ndy[0, 1]
如果想进行切片, 可以使用:
1
2
3
4
5
6
7
8
9
10
b = [[[1,2],[2,3],[3,4]], [[4,5],[5,6],[6,7]]]
b_ndy = np.array(b, dtype=int)
print(b_ndy[:, :, 0])
print(b_ndy[0:, 1:2, 1])
# 输出
# [[1 2 3]
# [4 5 6]]
# [[3]
# [6]]
- 单独的
:
表示选择该维度的所有元素 a:
表示该维度下索引大于等于a的所有元素:b
表示该维度下索引小于b的所有元素(不包含等于b)a:b
即以上两个限定的叠加
也可以使用...
代替若干个:
, 不过这仅适用于只在”两头的维度”存在限制的情况:
1
2
3
b_ndy[0:, ... , 1]
b_ndy[0:, ... ]
b_ndy[... , 1]
有时普通的索引、切片无法满足要求, 为了写出更加优雅简介的代码, 我们使用更高级的索引方式
数组索引
我们从一个简单的例子开始
1
2
3
4
5
6
7
8
9
a = [[1, 2, 3], [4, 5, 6]]
a_ndy = np.array(a, dtype=int)
# 获取2行1列和1行2列的数
rows = (1, 0)
cols = (0, 1)
print(a_ndy[rows, cols])
# 输出
# [4 2]
有所领会了吗? 我们将维数增加到三:
1
2
3
4
5
6
7
8
9
10
b = [[[1,2],[2,3],[3,4]], [[4,5],[5,6],[6,7]]]
b_ndy = np.array(b, dtype=int)
# 获取(1,0,1)和(0,1,0)的数
dim1 = (1, 0)
dim2 = (0, 1)
dim3 = (1, 0)
print(b_ndy[dim1, dim2, dim3])
# 输出
# [5 2]
或许我们可以更加大胆一些, 获取一个二维数组:
1
2
3
4
5
6
7
8
9
10
a = [[1, 2, 3], [4, 5, 6]]
a_ndy = np.array(a, dtype=int)
# 获取2行1列和1行2列的数
rows = ((1, 0), (0, 1))
cols = ((0, 1), (1, 2))
print(a_ndy[rows, cols])
# 输出
# [[4 2]
# [2 6]]
其他教程中大多使用列表进行索引, 至于这里为什么使用元组而不是列表—-至少在我所使用的Python 3.6.9版本中, 这个功能呈现出过渡的状态:
1
2
3
4
5
6
b = [[[1,2],[2,3],[3,4]], [[4,5],[5,6],[6,7]]]
index = [[0,1], [0,1], [1,0]]
print(b_ndy[index])
# 输出
# [2 5]
输出在预料之中, 这样的调用会收到警告
1
2
FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
print(b_ndy[index])
也就是说, 这样的调用在未来会被认为传入ndarray
类型, 会使结果大为不同
我们尝试一下传入ndarray
类型, 而不是list
, 结果果然出乎意料(有兴趣的话请自己去试一试)
虽然直接把list传入不会报警告, 也会得到正确结果, 像下面这样:
1
2
3
4
5
6
b = [[[1,2],[2,3],[3,4]], [[4,5],[5,6],[6,7]]]
b_ndy = np.array(b, dtype=int)
print(b_ndy[[0,1], [0,1], [1,0]])
# 输出
# [2 5]
但是Python推荐使用元组来做索引, 所以正确的姿势应该是这样的
1
2
3
4
5
6
7
b = [[[1,2],[2,3],[3,4]], [[4,5],[5,6],[6,7]]]
b_ndy = np.array(b, dtype=int)
index = ((1, 0), (1, 0), (0, 1))
print(b_ndy[index])
# 输出
# [2 5]
对于基本索引和高级索引的结合, 这是一个比较复杂的问题, 有兴趣的话可以去查阅numpy文档的相关内容
布尔索引(条件索引)
这是一个很实用的索引方法, 可以帮我们省去不少遍历判断的代码
1
2
3
4
5
6
7
8
9
10
11
12
# 数组名[条件表达式]
x = [[1,2,3],
[4,5,6],
[7,8,9]]
x_ndy = np.array(x, dtype=int)
x_ndy[x_ndy < 5] = 5
print(x_ndy)
# 输出
# [[5 5 5]
# [5 5 6]
# [7 8 9]]
除了直接写条件表达式之外, 我们还可以使用numpy.where()
(以下简称where()
)实现条件索引
where()
有两种情况:
- where(condition, x, y): 条件为真则将对应位直为x, 为假则置为y
- where(condition): 返回条件为真的数组下标
第二种情况的一个例子:
1
2
3
4
5
6
7
8
9
10
x = np.array([[1,5,3],[2,4,1]], dtype=int)
where_x = np.where(x < 4)
x[where_x] = 4
print(where_x)
print(x)
# 输出
# (array([0, 0, 1, 1]), array([0, 2, 0, 2]))
# [[4 5 4]
# [4 4 4]]
其实这里where()
的返回值用的就是上文数组索引的方法
2.5 ndarray
运算
常见的运算符可以直接作用于ndarray
对象的每一个元素
他们有+
, -
, *
, /
, **
, +=
, -=
, *=
, **=
等(注意/=
会出现语法错误)
它们可以作用于相同结构的ndarray
之间(各个对应), 也可以作用于ndarray
与数之间:
1
2
3
4
5
6
7
8
9
10
11
12
b = [[1,2],[2,3]]
c = [[2,3],[4,5]]
b_ndy = np.array(b, dtype=int)
c_ndy = np.array(c, dtype=int)
print(b_ndy + c_ndy)
print(b_ndy + 3)
# 输出
# [[3 5]
# [6 8]]
# [[4 5]
# [5 6]]
除运算符之外, 还有一些内置函数
它们都可以用axis=
指定维度, 若不指定, 就将全部元素一视同仁, 被指定的维数在运算后会消失
1
2
3
4
5
6
7
8
9
10
b = [[1,2],[4,3]]
b_ndy = np.array(b, dtype=int)
print(b_ndy.sum())
print(b_ndy.sum(axis=0))
print(b_ndy.sum(axis=1))
# 输出
# 10
# [5 5]
# [3 7]
除了sum()
之外, 还有max()
, min()
, 中值median()
, 标准差std()
此外, 还有numpy.argmin()
, numpy.argmax()
, 它们用来找最小值/最大值对应的下标:
(以argmin()
为例)
1
2
3
4
5
6
x = np.array([[1,5,3],[2,4,1]], dtype=int)
argmin_x = np.argmin(x, axis=0)
print(argmin_x)
# 输出
# [0 1 1]
当然, numpy中包含了很多数学方面的函数, 这里就不赘述了
2.6 修改ndarray
这部分的内容较多, 推荐在菜鸟教程各取所需
菜鸟教程不一定正确, 还请各位看官发扬实践精神, 毕竟实践才是检验真理的唯一标准