ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

numpy迭代数组nditer的实现示例

numpy迭代数组nditer的实现示例 前言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 npa np.arange(6).reshape(2, 3)for x in np.nditer(a):print(x, end )# 官方文档给出的输出0 1 2 3 4 5这里最关键的一点是默认的访问顺序不是「C 序」也不是「F 序」而是「和数组在内存里的布局一致」orderK。官方文档对此的解释是默认情况下人们只想拿到每个元素而不关心具体顺序所以按内存布局访问效率最高。这一点用转置来解释最清楚转置返回的是视图它的内存布局并没有改变所以对a.T直接迭代访问顺序和对a迭代是一样的# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.arange(6).reshape(2, 3)for x in np.nditer(a.T):print(x, end )# 与迭代 a 相同的顺序0 1 2 3 4 5for x in np.nditer(a.T.copy(orderC)):print(x, end )# 对转置做了一份 C 序副本后布局变了顺序也就变了0 3 1 4 2 5最后这个对比说明了一件事顺序跟着内存布局走。想改变顺序要么改布局.copy(orderC)要么显式指定order参数。二、op_flags默认只读要改就得声明nditer默认把输入当作只读对象。这不是可有可无的细节——如果你在循环体里给元素赋值而不声明写权限改动不会生效或者行为不符合预期。要修改必须用每个操作数的op_flags声明为readwrite或writeonly。更要注意的是写回时机nditer在迭代时用的是缓冲区改动要在迭代结束后才写回原数组。所以你必须显式告诉它「迭代结束了」两种方式任选其一用with语句把它当上下文管理器退出时自动写回或者手动调用迭代器的close()方法。而且一旦close()被调用或with块结束这个迭代器就再也不能继续迭代了。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa 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 npit np.nditer(a, op_flags[readwrite])try:for x in it:x[...] 2 * xfinally:it.close()op_flags里常见的有readonly只读、readwrite读写、writeonly只写、copy允许迭代器生成临时副本、allocate为None操作数分配空间。传多个操作数时op_flags要写成列表的列表每个内层列表对应一个操作数。三、orderC 序、F 序、K 序order参数有三种取值取值含义什么时候用K保持数组原有的内存布局顺序默认只想高效地访问每个元素C按 C 序最后一个轴变化最快要求行为与按行优先存储一致时F按 Fortran 序第一个轴变化最快列优先的场景或与 Fortran 代码对接对比一下同一个数组在K和F下的差别# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa np.arange(6).reshape(2, 3)for x in np.nditer(a, orderF):print(x, end )# 官方文档给出的输出0 3 1 4 2 5记忆方式orderF让第一根轴走得最慢所以先取到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 npa 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.indexflags[multi_index]打开的是多维下标此时读it.multi_index。有个约束一定要记住索引跟踪和external_loop不能同时用。原因很直白——外部循环一次给你一整块而索引是「每个元素一个值」两者冲突。官方文档明确写了同时指定会抛异常错误信息是EXTERNAL_LOOP不能被使用。如果你需要「下标 成块」的组合只能自己用切片或向量化来实现。external_loop与buffered一次拿一块默认情况下nditer一次给你一个元素。external_loop标志改变这一点它把「外部循环」交给调用方一次交给你一个一维的块。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npa 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], orderF):print(x, end )# 官方文档给出的输出[0 3] [1 4] [2 5] —— 三块为什么强制F序之后块变小了因为按 Fortran 序访问一个 C 序存储的数组时元素之间没有一个恒定的跨距迭代器没法把它们凑成一个连续块只能切成几段。这就是buffered出场的地方打开缓冲后迭代器会先把数据拷进缓冲区、按你要的顺序排好再交给你于是块又能变大。官方文档给的对比很典型同样的orderF加上buffered之后[0 3] [1 4] [2 5]变成了[0 3 1 4 2 5]一整块。# 适用于 Python 3.8 且已安装 NumPy以官方文档为准import numpy as npfor x in np.nditer(a, flags[external_loop, buffered], orderF):print(x, end )# 官方文档给出的输出[0 3 1 4 2 5]官方文档提醒这种「块变小」的问题在写 C 代码时通常无所谓但在纯 Python 代码里会显著增加解释器开销——块越小你的循环体被调用的次数越多。所以如果你必须用external_loop配上buffered往往是对的。buffered还有一个用途按指定数据类型迭代。用op_dtypes指定目标类型迭代器会通过临时副本或缓冲区把数据以该类型交给你而不必自己在循环里做转换。官方文档也提示了两条权衡临时副本的缺点是可能占用大量内存尤其是目标类型比原类型更大时而缓冲模式则会做分块拷贝。此外写成缓冲模式时要留意casting规则比如用castingsame_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 * xx是零维的缓冲数组直接重新绑定名字不会改动原数组。❌ 声明了readwrite但忘了退出上下文或调用close()改动没写回。✅ 写回发生在with块退出或close()调用时用try / finally保证一定会close()。❌ 把nditer当上下文管理器用完还想再迭代一次。✅ 一旦close()或退出with迭代器就不能再用了需要重新创建一个。❌ 同时指定索引跟踪f_index/multi_index和external_loop。✅ 这两个不能共存会抛异常需要「下标 成块」时改用切片或向量化实现。❌ 用了external_loop却没加buffered在orderF下拿到一堆小碎块。✅ 加上buffered让迭代器先把数据排好放进缓冲区块会变大Python 层循环次数随之减少。❌ 以为把for循环改写成nditer循环就会更快。✅nditer主要省的是索引开销纯 Python 的循环体不划算能向量化就直接向量化。❌ 对多个数组迭代时把op_flags写成一个扁平列表。✅ 传多个操作数时op_flags要写成「列表的列表」每个内层列表对应一个操作数。❌ 在nditer里做归约却不加reduce_ok。✅ 归约需要打开reduce_ok标志并给归约目标一个可写通常还带allocate的操作数。总结需求用法注意遍历顺序无关np.nditer(a)默认按内存布局即orderK指定遍历顺序orderC/F改变顺序会改变块的连续性修改元素op_flags[readwrite]x[...] ...退出with或close()时才写回取下标flags[f_index]读it.indexflags[multi_index]读it.multi_index不能与external_loop同用一次拿一块flags[external_loop]常配bufferedbuffered让块变大、减少 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本文代码未在本机运行仅作人工推演。
返回列表