前言
NumPy 是第三方库,用之前需要pip install numpy;本机没有 Python 解释器也没有装 NumPy,所以下面的示例无法在本机运行验证,只能逐行人工推演,行为描述以 NumPy 官方文档为准。
先说清楚nditer是用来干什么的,因为这决定了你要不要用它。nditer是 NumPy 的数组迭代器对象,从 NumPy 1.6 引入,官方文档说它「提供了很多灵活的方式,系统地访问一个或多个数组的所有元素」。它存在的原因是:用 Python 层的for循环遍历数组,每一次取元素都要经过解释器的开销;而nditer把迭代逻辑放在 C 层,一次取一个元素交给你的循环体,比逐层手动索引要省。
但这里有个常见的误解:「用了nditer就自动变快」。不一定。如果你在循环体里做的事很轻(比如只做一次加法),那么省下的那点索引开销根本抵不过 Python 层循环本身的成本——这时候正确的答案是「干脆别写循环,用向量化」。nditer真正的价值场景是两类:
- 你需要自己控制迭代过程——比如要在遍历时同时读取下标、按特定顺序访问、或者对多个数组做元素级别的配对运算;
- 你要和 C 扩展配合——官方明确说了,
nditer的 Python 接口是 C 数组迭代器 API 的一个直接映射,理解它有助于你在 C 或 C++ 里操作数组。
本文按「为什么用 → 基本用法 →op_flags→order→external_loop与buffered→ 适用场景」的顺序讲。
一、最基本的迭代与它的默认顺序
最简单的用法是把数组交给nditer,然后像普通可迭代对象一样循环:
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.arange(6).reshape(2, 3)
for x in np.nditer(a):
print(x, end=" ")
# 官方文档给出的输出:0 1 2 3 4 5这里最关键的一点是:默认的访问顺序不是「C 序」也不是「F 序」,而是「和数组在内存里的布局一致」(order='K')。官方文档对此的解释是:默认情况下人们只想拿到每个元素,而不关心具体顺序,所以按内存布局访问效率最高。
这一点用转置来解释最清楚:转置返回的是视图,它的内存布局并没有改变,所以对a.T直接迭代,访问顺序和对a迭代是一样的:
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.arange(6).reshape(2, 3)
for x in np.nditer(a.T):
print(x, end=" ")
# 与迭代 a 相同的顺序:0 1 2 3 4 5
for x in np.nditer(a.T.copy(order="C")):
print(x, end=" ")
# 对转置做了一份 C 序副本后,布局变了,顺序也就变了:0 3 1 4 2 5最后这个对比说明了一件事:顺序跟着内存布局走。想改变顺序,要么改布局(.copy(order="C")),要么显式指定order参数。
二、op_flags:默认只读,要改就得声明
nditer默认把输入当作只读对象。这不是可有可无的细节——如果你在循环体里给元素赋值而不声明写权限,改动不会生效(或者行为不符合预期)。要修改,必须用每个操作数的op_flags声明为'readwrite'或'writeonly'。
更要注意的是写回时机:nditer在迭代时用的是缓冲区,改动要在迭代结束后才写回原数组。所以你必须显式告诉它「迭代结束了」,两种方式任选其一:
- 用
with语句把它当上下文管理器,退出时自动写回; - 或者手动调用迭代器的
close()方法。
而且一旦close()被调用或with块结束,这个迭代器就再也不能继续迭代了。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.arange(6).reshape(2, 3)
with np.nditer(a, op_flags=["readwrite"]) as it:
for x in it:
x[...] = 2 * x
# 退出 with 时写回,a 变为 [[0, 2, 4], [6, 8, 10]]注意循环体里写的是x[...] = ...而不是x = ...。原因在于x是从迭代器拿到的零维数组(官方文档称之为缓冲数组),x = 2 * x只是把本地名字重新绑定到新对象上,原数组收不到任何改动;必须用x[...] = ...这种「对整体赋值」的写法才能真正写进去。这是nditer里最容易出错的一处。
with写法在较新的 NumPy 里都支持;如果你不确定自己的版本,用try / finally手动close()更保险:
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
it = np.nditer(a, op_flags=["readwrite"])
try:
for x in it:
x[...] = 2 * x
finally:
it.close()op_flags里常见的有:'readonly'(只读)、'readwrite'(读写)、'writeonly'(只写)、'copy'(允许迭代器生成临时副本)、'allocate'(为None操作数分配空间)。传多个操作数时,op_flags要写成列表的列表,每个内层列表对应一个操作数。
三、order:C 序、F 序、K 序
order参数有三种取值:
| 取值 | 含义 | 什么时候用 |
|---|
'K' | 保持数组原有的内存布局顺序(默认) | 只想高效地访问每个元素 |
'C' | 按 C 序(最后一个轴变化最快) | 要求行为与按行优先存储一致时 |
'F' | 按 Fortran 序(第一个轴变化最快) | 列优先的场景,或与 Fortran 代码对接 |
对比一下同一个数组在K和F下的差别:
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.arange(6).reshape(2, 3)
for x in np.nditer(a, order="F"):
print(x, end=" ")
# 官方文档给出的输出:0 3 1 4 2 5记忆方式:order='F'让第一根轴走得最慢,所以先取到a[0,0]=0、接着换行取a[1,0]=3,然后才是下一列。默认的'K'则完全跟随数据实际怎么放的——这就是为什么对a.T用默认序访问,顺序反而和a一致。
四、跟踪下标与成块迭代:f_index、multi_index、external_loop、buffered
迭代时如果要拿到当前元素的位置,用 iterator flag 打开索引跟踪,再读迭代器的index或multi_index属性:
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.arange(6).reshape(2, 3)
it = np.nditer(a, flags=["multi_index"])
for x in it:
print(x, it.multi_index, end=" ")
# 官方文档给出的输出:0 (0, 0) 1 (0, 1) 2 (0, 2) 3 (1, 0) 4 (1, 1) 5 (1, 2)flags=["f_index"]打开的是扁平索引(按 Fortran 序计数的那个一维下标),此时读it.index;flags=["multi_index"]打开的是多维下标,此时读it.multi_index。
有个约束一定要记住:索引跟踪和external_loop不能同时用。原因很直白——外部循环一次给你一整块,而索引是「每个元素一个值」,两者冲突。官方文档明确写了,同时指定会抛异常,错误信息是EXTERNAL_LOOP不能被使用。如果你需要「下标 + 成块」的组合,只能自己用切片或向量化来实现。
external_loop与buffered:一次拿一块
默认情况下nditer一次给你一个元素。external_loop标志改变这一点:它把「外部循环」交给调用方,一次交给你一个一维的块。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
a = np.arange(6).reshape(2, 3)
for x in np.nditer(a, flags=["external_loop"]):
print(x, end=" ")
# 官方文档给出的输出:[0 1 2 3 4 5] —— 一整块
for x in np.nditer(a, flags=["external_loop"], order="F"):
print(x, end=" ")
# 官方文档给出的输出:[0 3] [1 4] [2 5] —— 三块为什么强制F序之后块变小了?因为按 Fortran 序访问一个 C 序存储的数组时,元素之间没有一个恒定的跨距,迭代器没法把它们凑成一个连续块,只能切成几段。
这就是buffered出场的地方:打开缓冲后,迭代器会先把数据拷进缓冲区、按你要的顺序排好再交给你,于是块又能变大。官方文档给的对比很典型:同样的order="F",加上buffered之后,[0 3] [1 4] [2 5]变成了[0 3 1 4 2 5]一整块。
# 适用于 Python 3.8+ 且已安装 NumPy(以官方文档为准)
import numpy as np
for x in np.nditer(a, flags=["external_loop", "buffered"], order="F"):
print(x, end=" ")
# 官方文档给出的输出:[0 3 1 4 2 5]官方文档提醒:这种「块变小」的问题在写 C 代码时通常无所谓,但在纯 Python 代码里会显著增加解释器开销——块越小,你的循环体被调用的次数越多。所以如果你必须用external_loop,配上buffered往往是对的。
buffered还有一个用途:按指定数据类型迭代。用op_dtypes指定目标类型,迭代器会通过临时副本或缓冲区把数据以该类型交给你,而不必自己在循环里做转换。官方文档也提示了两条权衡:临时副本的缺点是可能占用大量内存(尤其是目标类型比原类型更大时);而缓冲模式则会做分块拷贝。此外,写成缓冲模式时要留意casting规则(比如用casting='same_kind'允许安全范围内的转换)。
五、什么时候该用它
回到开头的问题。nditer适合这些场合:
- 需要在遍历时同时用到下标,而且不想手写
np.ndindex之类的嵌套循环; - 需要对多个数组做元素级配对遍历(把多个数组一起传给
nditer,它会自动做广播,比手写索引清爽得多); - 需要在遍历中做归约(用
reduce_ok标志配合可写操作数); - 准备把这些逻辑下沉到 C 扩展里——因为
nditer的 Python 接口与 C 数组迭代器 API 一一对应,先在 Python 里用nditer把逻辑写清楚,再搬到 C 或 Cython 里,是很自然的路径。
它不适合这些场合:
- 只是想对数组做一个整体运算。直接用向量化表达式(
a * 2、a + b、np.where(...))比任何手写循环都快,因为向量化把循环放在编译好的 C 代码里跑,连「每次迭代回 Python 一次」的开销都省了。 - 只是想遍历元素、顺序无所谓、循环体也是纯 Python 的简单计算。这时候
nditer省下的索引开销有限,而 Python 层循环的成本还在。
一句话总结:nditer是「控制流工具」,不是「加速开关」。它的定位是让你在必须自己控制迭代时有一个 C 层的高效通道,而不是取代向量化。
常见坑点
- ❌ 在
with np.nditer(a, op_flags=["readwrite"]) as it:的循环体里写x = 2 * x。
✅ 必须写x[...] = 2 * x;x是零维的缓冲数组,直接重新绑定名字不会改动原数组。
- ❌ 声明了
readwrite但忘了退出上下文或调用close(),改动没写回。
✅ 写回发生在with块退出或close()调用时;用try / finally保证一定会close()。
- ❌ 把
nditer当上下文管理器用完,还想再迭代一次。
✅ 一旦close()或退出with,迭代器就不能再用了,需要重新创建一个。
- ❌ 同时指定索引跟踪(
f_index/multi_index)和external_loop。
✅ 这两个不能共存,会抛异常;需要「下标 + 成块」时改用切片或向量化实现。
- ❌ 用了
external_loop却没加buffered,在order="F"下拿到一堆小碎块。
✅ 加上buffered,让迭代器先把数据排好放进缓冲区,块会变大,Python 层循环次数随之减少。
- ❌ 以为把
for循环改写成nditer循环就会更快。
✅nditer主要省的是索引开销;纯 Python 的循环体不划算,能向量化就直接向量化。
- ❌ 对多个数组迭代时,把
op_flags写成一个扁平列表。
✅ 传多个操作数时op_flags要写成「列表的列表」,每个内层列表对应一个操作数。
- ❌ 在
nditer里做归约却不加reduce_ok。
✅ 归约需要打开reduce_ok标志,并给归约目标一个可写(通常还带allocate)的操作数。
总结
| 需求 | 用法 | 注意 |
|---|
| 遍历(顺序无关) | np.nditer(a) | 默认按内存布局,即order='K' |
| 指定遍历顺序 | order='C'/'F' | 改变顺序会改变块的连续性 |
| 修改元素 | op_flags=['readwrite']+x[...] = ... | 退出with或close()时才写回 |
| 取下标 | flags=['f_index']读it.index;flags=['multi_index']读it.multi_index | 不能与external_loop同用 |
| 一次拿一块 | flags=['external_loop'],常配'buffered' | buffered让块变大、减少 Python 层循环 |
| 按别的 dtype 迭代 | op_dtypes=[...]+buffered | 留意内存占用与casting规则 |
| 做归约 | flags=['reduce_ok'] | 目标操作数需可写 |
| 想提速 | 优先向量化 | nditer是控制流工具,不是加速开关 |
nditer的全部设计都围绕一个目标:在 C 层系统化地访问一个或多个数组的元素,并把「怎么迭代」的控制权交给你。记住默认只读、写回要显式结束、顺序默认跟随内存布局这三条,再按需打开external_loop和buffered,你就能用它写出既清晰又不浪费的迭代逻辑。
参考:nditer的引入版本、op_flags、order、迭代器标志与缓冲机制,均以 NumPy 官方文档的「Iterating over arrays」章节为准;NumPy 为第三方库,需pip install numpy;本文代码未在本机运行,仅作人工推演。