Posts Python中ndarray用法初步解析
Post
Cancel

Python中ndarray用法初步解析

本文运行环境: Python 3.6.9 64-bit, WSL: Ubuntu-18.04

一、 前言

Python中, numpy提供了强大的科学计算库, 其中ndarray是其中表示数组的类, 其全称为The N-dimensional array(N维数组)

这篇文章不够全面、仅供入门, 希望学习完这篇后能够在实践中逐渐上手numpy

这里推荐Github中的图像处理100问(中文版), 其中大量用到了ndarray, 参考答案中的代码也写得十分优雅, 感兴趣的看官不妨稍候移步于此

二、 正篇

2.1 限定条件

ndarraylist/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): 通过最值和步长创建, 前闭后开, 当存在一个参数为floatdtype=float
    • linspace(min, max, count): 通过最值和元素个数创建, 闭区间, dtype=float(无论是否整除)
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, 可以通过指定来改变底数
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

这部分的内容较多, 推荐在菜鸟教程各取所需

菜鸟教程不一定正确, 还请各位看官发扬实践精神, 毕竟实践才是检验真理的唯一标准

三、 参考资料

This post is licensed under CC BY 4.0 by the author.