Python / Numpy 遍历数组

Numpy 遍历数组

Numpy 提供了一个迭代器对象 numpy.nditer,能够实现灵活地访问一个或者多个数组元素,达到遍历数组的目的。

1. 数组元素访问

1.1 按照内存布局打印数组元素

在默认情况下,numpy.nditer 迭代器返回的元素顺序,是和数组内存布局一致的,这样做是为了提升访问的效率,默认是行序优先。

案例

例如,我们对于新创建的 2×3 的数组,利用 nditer 迭代器进行顺序访问:

arr = np.arange(6).reshape(2,3)
arr
Out: 
	array([[0, 1, 2],
           [3, 4, 5]])
for i in np.nditer(arr):
    print(i, end=" ")

打印结果为:

0 1 2 3 4 5 

可以看到,在不增加其他设置的情况下,默认的打印顺序是行序优先(即 C-order)。

在不改变内部布局的情况下,通过该方式进行遍历,并不会改变顺序,例如我们通过迭代上述数组的转置来证明这一点。

for i in np.nditer(arr.T):
    print(i, end=" ")

打印结果为:

0 1 2 3 4 5 

从上述结果可以看出,转置方法并未改变数组元素的存储顺序。

相对应的,我们利用 copy 方法,显式地更改内存顺序,nditer 迭代器的遍历解雇也会发生响应的变化:

for i in np.nditer(arr.T.copy("C")):
    print(i, end=" ")

打印结果为:

0 3 1 4 2 5 

1.2 控制遍历顺序

如果想要改变遍历的顺序,一种是上面案例中提到的,利用 copy 方法来修改内存顺序。当然,nditer 也提供了 order 参数来达到同样的目的。

案例

C order,即是行序优先,跟默认的遍历顺序一致。

print ('以 C 风格顺序排序:')
for i in np.nditer(arr, order="C"):
    print(i, end=" ")

打印结果为:

以 C 风格顺序排序:
0 1 2 3 4 5 

Fortran order,即是列序优先:

print ('以 F 风格顺序排序:')
for i in np.nditer(arr, order="F"):
    print(i, end=" ")

打印结果为:

以 F 风格顺序排序:
0 3 1 4 2 5 

2. 数组元素修改

nditer 对象有另一个可选参数 op_flags。 默认情况下,nditer 将视待迭代遍历的数组为只读对象(read-only),为了在遍历数组的同时,实现对数组元素值得修改,必须指定 read-write 或者 write-only 的模式。

案例

在遍历的时候,对数组进行平方计算,生成一个特殊的平方方阵。

arr1 = np.arange(16).reshape(4,4)
arr1
Out:
    array([[ 0,  1,  2,  3],
           [ 4,  5,  6,  7],
           [ 8,  9, 10, 11],
           [12, 13, 14, 15]])
# 指定读写模式
for i in np.nditer(arr1, op_flags=["readwrite"]):
    i[...] = i**2
arr1
Out:
    array([[  0,   1,   4,   9],
           [ 16,  25,  36,  49],
           [ 64,  81, 100, 121],
           [144, 169, 196, 225]])

在读写模式下,arr1 数组发生了变化。

3. flags 可选参数

flags 参数可以接受传入一个数组或元组,它可以接受下列值:

参数 描述
c_index 可以跟踪 C 顺序的索引
f_index 可以跟踪 Fortran 顺序的索引
multi-index 每次迭代可以跟踪多重索引类型
external_loop 给出的值是具有多个值的一维数组,而不是零维数组

3.1 可以跟踪 C 顺序的索引

list 类似,每个元素都对应有相应的 id。在按照 C 顺序跟踪索引的时候,数组的索引可以按照下图来直观理解:
图片描述

C 顺序的索引

上述索引的标注是按照行优先的顺序进行的。

案例

设置 flags=["c_index"],可以实现类似 list 的 enumerate 函数的效果:

cit =  np.nditer(arr, flags=["c_index"])
while not cit.finished:
    print("value:", cit[0], "index:<{}>".format(cit.index))
    cit.iternext()

打印结果为:

value: 0 index:<0>
value: 1 index:<1>
value: 2 index:<2>
value: 3 index:<3>
value: 4 index:<4>
value: 5 index:<5>

在上述代码中,同过 while 循环可以逐步打印出每个元素的值和索引。

3.2 可以跟踪 Fortran 顺序的索引

在按照 F 顺序跟踪索引的时候,数组的索引可以按照下图来直观理解:
图片描述

F 顺序的索引

F 顺序即列优先的顺序。

案例

想要实现该索引顺序,可以设置 flags=["f_index"]

fit =  np.nditer(arr, flags=["f_index"])
while not fit.finished:
    print("value:", fit[0], "index:<{}>".format(fit.index))
    fit.iternext()

打印结果为:

value: 0 index:<0>
value: 1 index:<2>
value: 2 index:<4>
value: 3 index:<1>
value: 4 index:<3>
value: 5 index:<5>

可以发现,在顺序打印该索引结构的时候,默认是按照行优先的顺序打印的。

也就是说,在打印索引结构的时候,打印的顺序是一样的,不同的地方在于,c_indexf_index 索引标注的顺序不一样。

3.3 多重索引

对于 arr 这样的二维数组,可以用 2 个维度(x 方向和 y 方向)的序列来唯一定位每一个元素,multi-index 则可以打印出该种索引顺序。

multi_index 索引类型可以按照下图来直观理解:
图片描述

multi 顺序的索引

案例

设置 flags=["multi_index"],效果如下:

mul_it = np.nditer(arr, flags=['multi_index'])

while not mul_it.finished:
    print("value:", mul_it[0], "index:<{}>".format(mul_it.multi_index))
    mul_it.iternext()

打印结果为:

value: 0 index:<(0, 0)>
value: 1 index:<(0, 1)>
value: 2 index:<(0, 2)>
value: 3 index:<(1, 0)>
value: 4 index:<(1, 1)>
value: 5 index:<(1, 2)>

3.4 遍历返回一维数组

将一维的最内层的循环转移到外部循环迭代器,使得 NumPy 的矢量化操作在处理更大规模数据时变得更有效率。简单来说,当指定 flags=['external_loop'] 时,将返回一维数组而并非单个元素。

具体来说,当 ndarray 的顺序和遍历的顺序一致时,将所有元素组成一个一维数组返回;当 ndarray 的顺序和遍历的顺序不一致时,返回每次遍历的一维数组。

下面通过具体案例来理解这句话:

案例

对于上述创建的 arr,是行优先顺序的数组。当我们指定遍历顺序为C(行优先,与定义的顺序一致),指定 flags=["external_loop"],则有:

for i in np.nditer(arr, flags=['external_loop'], order='C'):
    print(i)

打印结果为:

[0 1 2 3 4 5]

可以看到,该案例中,把全部元素组成一个一维数组,并返回。

案例

当我们指定遍历顺序为F(列优先),指定 flags=["external_loop"],则有:

for i in np.nditer(arr, flags=['external_loop'], order='F'):
    print(i)

打印结果为:

[0 3]
[1 4]
[2 5]

可以看到,该案例中,返回每次遍历的一维数组。

4. 小结

本节主要介绍了如何利用 Numpy 内置的迭代器对象 numpy.nditer,实现灵活地遍历数组中的元素。numpy.nditer 迭代器提供了 order 参数,来控制访问的顺序;提供了 op_flags 参数,来设置只读或读写模式;提供 flags 参数来同步返回数组索引,功能非常强大。