一维数组的遍历
一维数组的很多操作都和Python中的序列类型相同,其迭代可以直接使用for来完成遍历。
import numpy as npa = np.arange(5)for i in a:print(i, end=' ') # 0 1 2 3 4
多维数组的遍历
对于多维数组,不能简单地使用for来进行遍历,通过ndarray.flat,可以将多维数组「降维」,从而简单完成遍历。
import numpy as npa = np.array([[1, 2, 3], [6, 7, 8]])for i in a:print(i)# [1 2 3]# [6 7 8]"""使用 for 进行遍历,它只处理了第一个维度,并不是按元素进行的遍历操作如果我们知道数组的维度情况,就可以采用多个 for 循环嵌套来完成元素遍历或者采用递归的写法"""def my_traverse(array):for i in array:if isinstance(i, np.ndarray):my_traverse(i)else:print(i)
ndarray.flat返回了数组对象的迭代器,通过这个迭代器,就可以逐元素遍历。在 「Learning Python」中,已经详细讲解了迭代器和迭代协议,下面直接通过例子说明
a = np.array([[1, 2, 3], [6, 7, 8]])iter = a.flatprint(type(iter)) # <class 'numpy.flatiter'>print(next(iter)) # 1print(next(iter)) # 2for data in iter:print(data, end=' ') # 3 6 7 8
flat有「平整」的意思,这里可以理解为,将数组摊平到了一个维度上。
a = np.array([[1, 2, 3], [6, 7, 8]])"""就像操作一维数组一样"""print(a.flat[3], a.flat[4]) # 6 7a.flat[0] = 666print(a)# [[666 2 3]# [ 6 7 8]]
更通用的遍历方法
numpy.nditer是一种更加灵活的迭代器对象。
import numpy as npa = np.arange(6).reshape(2, 3)print(a)# [[0 1 2]# [3 4 5]]for i in np.nditer(a):print(i, end=' ') # 0 1 2 3 4 5
控制遍历顺序
我们可以选择遍历数组对象的顺序方式。默认的情况是根据数组元素在内存中的存储「模式」而定的,这样做更能提升访问效率,而并不是我们直观上的先遍历行元素,再遍历列元素。
b = a.T # 转置print(b)# [[0 3]# [1 4]# [2 5]]"""遍历结果和 a 的是一样的,说明通过转置计算得到的 b在内存上的元素存储「模式」是一样的"""for i in np.nditer(b):print(i, end=' ') # 0 1 2 3 4 5
比较常用的遍历顺序有C-order和F-order,前者是我们常见的行序优先,后者则是列序优先,现在用这两种方式来遍历b数组,再和flat方式做个对比:
for i in np.nditer(b, order='C'): # C styleprint(i, end=' ') # 0 3 1 4 2 5for i in np.nditer(b, order='F'): # Fortran styleprint(i, end=' ') # 0 1 2 3 4 5for i in b.flat:print(i, end=' ') # 0 3 1 4 2 5 说明 flat 是按照 C style 将元素「摊平」的
修改元素值
除了指定遍历元素的方式以外,还可以在遍历的过程中对数组元素进行修改。默认的遍历操作都是只读的,可以通过readwrite和writeonly将操作设定为读写或者只读模式。
nditer首先会生成可写的数组缓冲区,然后将修改后的元素值拷贝回原来的数组。因此,我们必须要告知迭代结束,以便后续元素拷贝。这里使用上下文管理器:
with np.nditer(a, op_flags=['readwrite']) as it:for i in it:i[...] = i + 1 # 修改元素值print(a)# [[1 2 3]# [4 5 6]]
i[...]的写法在「ndarray对象」那一小结提到过,目的就是为了修改它的值。这里感觉上可能会有些矛盾,因为给变量的赋值,只是将变量指向了新的地方,并未在原来内存空间上进行修改,所以直接常规的赋值方式并不能更改数组中的元素值。
在上面的例子中,因为i本质上是一个零维数组,通过上面的方法才能够正确修改其值。参考:What does “three dots” in Python mean when indexing what looks like a number?
看看使用flat迭代修改会怎么样:
import numpy as npa = np.arange(6).reshape(2, 3)for i in a.flat:i += 3 # 这里 i 的本质是整数,无法在原处修改值print(a)# [[0 1 2]# [3 4 5]]
易错点
在使用 Numpy 数组进行遍历更新时,可能凭直觉,我们都会写出这样的代码:
import numpy as npa = np.arange(6).reshape(2, 3)for i in a:i += 3 # 为什么不写 i = i + 3 我就不解释了print(a)# [[3 4 5]# [6 7 8]]
哟,完蛋,一不小心对数组进行了遍历, 而且把每个元素的值更新成功了。既然如此,为什么前面讲了一堆很复杂的东西呢?
虽然这里结果是正确的,但是程序并不是按照我们所想的逻辑运行出来的——它并没有对每一个元素进行遍历。在最开头,我们就知道了,直接使用 for 来遍历,程序只会对数组的第一维进行处理。
仔细思考下,循环里的 i 表示的是什么?
可以插入代码试验一下:
a = np.arange(6).reshape(2, 3)for i in a:print(i)# <class 'numpy.ndarray'># <class 'numpy.ndarray'>
首先,我们知道,数组只被访问了两次;其次,我们操作的对象 i ,它是 Numpy 子数组。所以,上面加法的本质是这样的:
# 第一次访问i = [0, 1, 2]i += 3# 第二次访问i = [3, 4, 5]i += 3
那为啥结果是对的?这就是前面讨论过的 Numpy 的广播机制 。 3 被「扩展」成了 [3, 3, 3] 来和子数组相加。所以, Numpy 数组的遍历操作没有我们想的那么简单,如果我们的需求仅仅是每个元素 +3 这么简单,当然可以这么来处理,但是要知道其背后的原理。
如果要处理更复杂的元素遍历修改操作(比如,奇数元素+1;偶数元素-1),就还是要借助迭代器这种比较复杂的方法来处理。
