Skip to content

NumPy - 遍历数组

在处理数组时,遍历元素是一种常见的需求。虽然标准的 Python for 循环可以用于 NumPy 数组,但对于大型数组,尤其是多维数组,它们通常效率不高。NumPy 提供了一个高度优化的迭代器对象 numpy.nditer(NumPy 迭代器对象),用于高效地进行多维迭代。

np.nditer 允许你使用 Python 的标准迭代器协议(for ... in ...)访问数组中的每个元素。默认情况下,它选择与数组内存布局匹配的迭代顺序,以获得更好的性能。

import numpy as np
a = np.arange(0, 60, 5).reshape(3, 4)
print(f"Original array:\n{a}\n")
print("Iterating elements using np.nditer:")
for x in np.nditer(a):
print(x, end=' ')
print() # 换行

输出(默认顺序通常是 C 风格,即行主序):

Original array:
[[ 0 5 10 15]
[20 25 30 35]
[40 45 50 55]]
Iterating elements using np.nditer:
0 5 10 15 20 25 30 35 40 45 50 55

默认的迭代顺序取决于数组在内存中的存储方式。迭代一个转置后的数组(a.T)可能会以不同的顺序生成元素,如果转置改变了内存的连续性。

import numpy as np
a = np.arange(0, 60, 5).reshape(3, 4)
print(f"Original array (C-order layout):\n{a}\n")
print("Transpose of the array:")
b = a.T # 转置
print(f"{b}\n")
print("Iterating over the transpose 'b':") # 基于底层数据,顺序可能仍然是 C 风格的
for x in np.nditer(b):
print(x, end=' ')
print()

输出:

Original array (C-order layout):
[[ 0 5 10 15]
[20 25 30 35]
[40 45 50 55]]
Transpose of the array:
[[ 0 20 40]
[ 5 25 45]
[10 30 50]
[15 35 55]]
Iterating over the transpose 'b':
0 5 10 15 20 25 30 35 40 45 50 55

注意:即使 b 是转置后的数组,如果 C 风格遍历底层数据更高效,np.nditer 可能仍然选择 C 风格的遍历方式。实际的顺序取决于内存布局。

你可以使用 order 参数强制 np.nditer 遵循特定的迭代顺序(C 风格/行主序 或 F 风格/列主序)。

  • order='C': 强制 C 风格顺序(行主序)。
  • order='F': 强制 Fortran 风格顺序(列主序)。
  • order='A': 如果数组是 F 连续的(F-contiguous),则以 Fortran 顺序迭代;否则以 C 顺序迭代。
  • order='K': 按照元素在内存中的布局顺序进行迭代(保持顺序 - 默认)。
import numpy as np
a = np.arange(0, 60, 5).reshape(3, 4)
print(f"Original array:\n{a}\n")
print("Iterating in C-style order (row-major):")
for x in np.nditer(a, order='C'):
print(x, end=' ')
print('\n')
print("Iterating in F-style order (column-major):")
for x in np.nditer(a, order='F'):
print(x, end=' ')
print()

输出:

Original array:
[[ 0 5 10 15]
[20 25 30 35]
[40 45 50 55]]
Iterating in C-style order (row-major):
0 5 10 15 20 25 30 35 40 45 50 55
Iterating in F-style order (column-major):
0 20 40 5 25 45 10 30 50 15 35 55

如果数组以 Fortran 顺序显式创建或复制,np.nditer(使用默认的 ‘K’ 或显式的 ‘F’ 参数)将高效地进行列向迭代。

import numpy as np
a = np.arange(0, 60, 5).reshape(3, 4)
b = a.T # 转置
print("Iterating 'b' (transpose) with default ('K') order:")
# 仍然以 C 风格遍历原始数据布局
for x in np.nditer(b):
print(x, end=' ')
print('\n')
print("Creating a copy of 'b' in F-order ('c = b.copy(order='F')'):")
c = b.copy(order='F') # 现在 'c' 确实是 F 连续的
print(f"Is 'c' F-contiguous? {c.flags.f_contiguous}")
print("Iterating 'c' with default ('K') order (now efficient F-style):")
for x in np.nditer(c):
print(x, end=' ')
print()

输出:

Iterating 'b' (transpose) with default ('K') order:
0 5 10 15 20 25 30 35 40 45 50 55
Creating a copy of 'b' in F-order ('c = b.copy(order='F')'):
Is 'c' F-contiguous? True
Iterating 'c' with default ('K') order (now efficient F-style):
0 20 40 5 25 45 10 30 50 15 35 55

默认情况下,np.nditer 将数组元素视为只读。要在迭代期间修改元素,你需要指定相应的 op_flags(操作标志)参数。常用的值有 ['readonly'](默认)、['readwrite'] 或 ['writeonly']。

修改时,你需要使用省略号 (...) 来为迭代器产生的元素赋值:x[...] = value。

import numpy as np
a = np.arange(0, 60, 5).reshape(3, 4)
print(f"Original array:\n{a}\n")
# 迭代并修改元素(乘以 2)
for x in np.nditer(a, op_flags=['readwrite']):
x[...] = 2 * x
print(f"Modified array:\n{a}")

输出:

Original array:
[[ 0 5 10 15]
[20 25 30 35]
[40 45 50 55]]
Modified array:
[[ 0 10 20 30]
[ 40 50 60 70]
[ 80 90 100 110]]

flags=['external_loop'] 选项会改变迭代器的行为。它不是生成单个元素(0 维数组),而是生成数组的一维块(chunks)。这可以提高某些操作的效率,因为它减少了 Python 循环的开销。

块的大小由缓冲决定,并且迭代遵循指定的 order 参数。

使用外部循环和 F 顺序(列主序)遍历列:

import numpy as np
a = np.arange(0, 60, 5).reshape(3, 4)
print(f"Original array:\n{a}\n")
print("Iterating with external_loop (F-order yields columns as 1D arrays):")
for x in np.nditer(a, flags=['external_loop'], order='F'):
print(x, end=' ')
print()

输出:

Original array:
[[ 0 5 10 15]
[20 25 30 35]
[40 45 50 55]]
Iterating with external_loop (F-order yields columns as 1D arrays):
[ 0 20 40] [ 5 25 45] [10 30 50] [15 35 55]

如果你向 np.nditer 提供多个可以广播(broadcast)到共同形状的数组,迭代器可以并发地遍历它们,生成对应的元素元组。

同时迭代一个 3x4 的数组 a 和一个 1x4 的数组 b。迭代期间,b 将被广播以匹配 a 的形状。

import numpy as np
a = np.arange(0, 60, 5).reshape(3, 4)
print(f"First array 'a':\n{a}\n")
b = np.array([1, 2, 3, 4], dtype=int)
print(f"Second array 'b': {b}\n")
print("Iterating over 'a' and broadcast 'b' concurrently:")
for x, y in np.nditer([a, b]):
print(f"{x}:{y}", end=' ')
print()

输出:

First array 'a':
[[ 0 5 10 15]
[20 25 30 35]
[40 45 50 55]]
Second array 'b': [1 2 3 4]
Iterating over 'a' and broadcast 'b' concurrently:
0:1 5:2 10:3 15:4 20:1 25:2 30:3 35:4 40:1 45:2 50:3 55:4