资讯动态

NumPy NEP 21 深度解析:用 `oindex` 与 `vindex` 简化显式高级索引

发布时间:2026/9/20 2:30:13 来源:尧图企业网站定制
科学计算数据分析【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址https://gitcode.com/gh_mirrors/nu/numpy点击查看免费下载本指南基于 NumPy 官方增强提案 NEP 21 — Simplified and explicit advanced indexing状态Deferred作者Sebastian Berg、Stephan Hoyer创建于 2015-08-27。它系统梳理了 NumPy 高级索引advanced indexing又称 fancy indexing的复杂规则与痛点并提出通过arr.oindex、arr.vindex两个显式索引属性来消除歧义的完整方案。读完本文你将理解基本索引与高级索引的边界、混合索引的转置规则、外索引outer indexing与向量化索引vectorized indexing的本质冲突并掌握 NEP 21 提出的全部设计规则、示例输出与动机场景从而在实际编码中写出无歧义、可移植的索引表达式。1. 为什么需要一次索引革命NEP 21 的背景用数组去索引数组是 NumPy 最强大、也最受欢迎的特性之一但现有的高级索引规则在多数组索引场景下对新手甚至老用户都造成了普遍的困惑。NEP 21 的核心主张是对高级索引进行一次彻底的重构与简化引入两个新的索引属性oindex外索引与vindex向量化索引让用户显式声明自己想要的索引语义而不是依赖一套隐式、容易出错的转置规则。1.1 现有索引操作的两大阵营NumPy 数组当前支持的索引操作分为两类NEP 原文称之为legacy indexing即传统索引类型构成要素典型示例返回语义基本索引Basic切片、整数、np.newaxis、省略号...x[0, :3, np.newaxis]始终返回视图view高级索引Advanced / Fancy用数组索引数组x[x 0]、x[[0, 1]]始终返回副本copy其中高级索引又细分为三种布尔索引Boolean indexing用布尔数组索引例如x[x 0]选取所有正数元素向量化索引Vectorized indexing用一个或多个整数数组索引例如x[[0, 1]]选取第一轴前两个元素。当使用多个数组时向量化索引借助广播broadcasting规则将多个维度的索引组合起来可以产出任意形状、包含原数组任意元素的结果混合索引Mixed indexing上述类型的任意组合本身并不比向量化索引更强但有时更便捷。1.2 缺失的一环外索引Outer / Orthogonal Indexing有一类被广泛需要的索引操作在 NumPy 中并未直接支持外索引outer/orthogonal indexing把一维数组视同切片来参与输出形状的确定。其规则是结果应当等价于沿每个维度用整数数组或布尔数组独立地索引仿佛被索引数组与索引数组都是一维的。这种索引形式对 MATLAB、Fortran、R 用户来说非常熟悉。NumPy 之所以不支持它是因为外索引与向量化索引的规则相互冲突。以二维数组被两个一维整数数组索引为例x[[0, 1], [0, 1]]外索引等价于用itertools.product()组合多个整数索引结果是 2D 数组包含所有索引元素的组合np.array([[x[0, 0], x[0, 1]], [x[1, 0], x[1, 1]]])向量化索引等价于用zip()组合多个整数索引结果是 1D 数组只包含对角元素np.array([x[0, 0], x[1, 1]])。这一差异是新手频繁踩坑的根源。外索引模型更易理解是切片规则的自然推广但 NumPy 选择了向量化索引因为它严格更强可以表达任意元素的任意组合。好消息是外索引总可以用向量化索引配合恰当的索引数组来模拟。为了降低门槛NumPy 提供了np.ogrid与np.ix_等工具例如x[np.ix_([0, 1], [0, 1])]。但没有任何工具能够模拟完全通用的混合外索引——即同时无歧义地容纳切片、整数、一维布尔数组与一维整数数组的混合形式。这正是 NEP 21 想填补的空白。1.3 混合索引为何如此令人困惑NumPy 现有的在同一操作中组合多种索引类型的规则相当复杂涉及大量边界情况。NEP 21 指出了一个关键陷阱混合索引乍看之下表现得像外索引实则不是。回到 2D 数组的例子x[:2, [0, 1]]和x[[0, 1], :2]都返回与原数组轴序相同的 2D 数组看起来人畜无害。但一旦出现两个或更多非切片对象包括整数向量化索引规则就开始生效由数组索引引入的轴被放在最前面——除非所有数组索引连续相邻此时 NumPy 才会猜测用户期望它们所在的位置。考虑形状为(X, Y, Z)的 3D 数组arrarr[:, [0, 1], 0]形状为(X, 2)—— 直观符合外索引直觉arr[[0, 1], 0, :]形状为(2, Z)—— 同样直观arr[0, :, [0, 1]]形状为(2, Y)而不是 (Y, 2)—— 这个反直觉的结果连许多资深 NumPy 用户都会踩坑。涉及多个数组索引的混合情形同样反直觉只是由于当前行为过于无用而很少被实际使用问题才不那么显眼。例如当布尔数组索引与另一个布尔或整数数组索引混合时布尔数组会被转换成整数数组索引等价于np.nonzero()然后参与广播索引一个形状为(2, 2)的 2D 数组x[[True, False], [True, False]]会得到形状为(1,)的 1D 向量而非形状为(1, 1)的 2D 子矩阵。更难回避的是当索引数少于数组维度时NumPy 会隐式补上完整切片因此x[[0, 1]]等价于x[[0, 1], :]。这类情形本身不令人意外但它反过来约束了混合索引行为的设计空间让人想说不用混合索引都不行。1.4 其他 Python 数组库的困境索引是访问多维数组数据普遍认可且广泛使用的机制科学 Python 生态中的许多库也都支持数组索引。但 NumPy 索引规则的完整复杂性意味着其他库想逐条复刻其行为既困难又不划算——NumPy 风格索引的唯一完整实现就是 NumPy 本身。以 dask.array 和 h5py 为例它们在某种程度上支持大多数索引类型其余部分则尽力照搬 NumPy 的 API基于非 NumPy 存储后端的库要实现向量化索引尤其困难反之沿至少一个维度用 1D 数组做外索引风格的索引则现实得多这导致 dask、h5py 等库尝试定义安全的 NumPy 风格索引子集例如只允许沿至多一个维度用数组索引但这很难做得足够通用且正确——NEP 指出当时版本的 dask 与 h5py 在处理上文案例 3 时都与 NumPy 行为不一致极易引发 bug。这些不一致意味着要写出 xarray、dask.array 这类能够互换索引多种数组存储的高层库非常困难。反过来如果 NumPy 提供显式的外索引与向量化索引 API外部库就有了可靠模仿的范本即使它们不支持每一种索引类型。2. 高层变更三个核心提议受 pandas 中通过多个indexer属性控制不同索引行为的启发NEP 21 提出引入arr.oindex[indices]允许数组索引但使用外索引逻辑引入arr.vindex[indices]使用当前的向量化/广播逻辑但与 legacy indexing 相比有两点不同不支持布尔索引——所有索引必须是整数、整数数组或切片整数索引产生的结果维度总是结果的第一个轴不做任何转置即使是单个整数数组索引也是如此。普通索引arr[...]将开始给出警告并最终报错在应当优先使用显式索引器indexer的场景下首先在所有 legacy 索引与外索引结果不同的情况下之后可能扩展到所有涉及整数数组的情况。这些约束足以让索引行为与用户直觉大体一致并通过oindex提供更平缓的学习曲线。需要特别注意的是上述所有内容对赋值assignment与取值subscription同样适用。NEP 21 作者也坦言理解这些细节并不容易——建议读者先看 Motivational Example动机示例再看讨论部分的代码示例。3. 详细规则从三大问题推导出的七条提案从前面指出的三个问题外索引缺失、向量化索引混乱、混合索引复杂可以推导出 NEP 21 对 NumPy 的七条具体期望应当有一个突出的外/正交索引方法如arr.oindex[indices]考虑到向量化/fancy 索引的混乱程度应当能通过显式方法如arr.vindex[indices]来表达新的arr.vindex[indices]不应受 fancy 索引转置规则的束缚——例如单个高级索引这种简单情形根本不该转置。因此不做任何转置由整数数组索引创建的轴总是插入到最前面即使只有一个索引也是如此布尔索引在概念上属于外索引。将布尔索引与其它高级索引按 legacy 方式广播通常既无帮助也不够明确希望获得nonzero加广播行为的用户应当手动实现。因此vindex不需要支持布尔索引数组应当实现arr.legacy_index属性以支持传统索引为存量代码迁移提供简单途径从而降低普通索引行为弃用deprecation的难度。故意选用更长的名字legacy_index就是为了显式地不鼓励新代码使用它普通索引arr[...]应对歧义情形报错。初期大致是arr[ind]与arr.oindex[ind]结果不同的情况给出弃用警告。这覆盖了所有使用多个整数数组的向量化索引。由于转置行为的存在这意味着arr[0, :, index_arr]会被弃用而arr[:, 0, index_arr]暂时不会为确保重写了索引行为的ndarray子类不会无意间回退到索引属性的默认行为这些属性应带有显式检查若__getitem__或__setitem__被重写则禁用这些属性。与普通索引不同新索引属性明确面向更高维度的索引因此还需实施若干附加变更强制精确的维度与索引匹配不再隐式添加省略号...。除非显式写出省略号否则索引表达式只对特定维数的数组有效。这使表达式更明确并能防止数组维数错误。对 Python 内置序列的鸭子类型兼容性没有影响因为 Python 序列只支持整数与切片的有限基本索引形式只接受元组当前普通索引允许用非元组进行多维索引例如arr[[slice(None), 2]]这造成了一些不一致。新索引属性应只允许普通 Python 元组用于多维索引普通索引是否也应如此是另一个问题新属性不应通过getitem来实现setitem因为这是一种权宜之计对向量化索引并不好用NEP 标注not implemented yet。3.1 开放问题Open QuestionsNEP 21 明确列出若干尚未定论的问题oindex、vindex、legacy_index的名字只是撰写时的建议NumPy 此前对类似oindex的概念用过np.ix_这个名字oindex与vindex可以始终返回副本即使未发生数组操作。支持返回视图的理由是oindex因此能作为通用索引替代品支持返回副本的理由则是arr.vindex[array_scalar, ...]中的array_scalar本应是 0-D 数组却往往不是0-D 数组容易被转换总是复制可以修复这种不一致普通索引的最终形态在本 NEP 中未定例如未来arr[index]有可能等价于arr.oindex。由于此类变更需要数年时间现阶段不必仓促决策对普通索引的变更可以无限期推迟或干脆不做以避免破坏现有代码库或迫使它们进行大规模修复。3.2 备选命名方案NEP 21 给出了三组候选命名后续还会增加建议语义方案一方案二Orthogonal正交/外索引oindexoixVectorized向量化索引vindexvixLegacy传统索引legacy_indexl/findex4. 子类兼容性显式检查与NotImplementedError子类在新索引属性面前有些棘手。NEP 21 给出的解决思路是对大多数子类未提供__getitem__或__setitem__的这些特殊属性应当直接可用提供__getitem__/__setitem__的子类必须相应更新且最好不要继承oindex与vindex所有子类都会继承这些属性但属性内部的__getitem__实现应当测试subclass.__getitem__ is ndarray.__getitem__。若不是说明该子类对索引有特殊处理此时应抛出NotImplementedError要求该子类显式重写索引属性__setitem__的实现同样应检查__setitem__是否被重写。进一步的问题是如何方便地实现这些特殊属性。另外__setitem__对非高级索引会调用__getitem__这一怪癖在新属性上或许应该避免但那样做又可能让事情更令人困惑。为便于实现可以考虑提供类似operator.itemgetter和operator.setitem的函数或提供 mixin 辅助类——这些属于后续工作并非初始实现所必需。5. 实现计划、向后兼容与备选方案5.1 实现路径实现将从编写可通过arr.oindex、arr.vindex、arr.legacy_index访问的特殊索引对象开始让这些索引操作可用同时开始弃用那些存在歧义的普通索引操作。此外NumPy 代码库自身需要使用新属性测试也需要相应调整。5.2 向后兼容作为新特性vindex与oindex属性不会带来向后兼容问题为最大程度保持兼容NEP 21 预期一个漫长的弃用周期并提出legacy_index属性作为过渡潜在的前向兼容问题主要出在未专门实现新方法的子类上。5.3 备选方案为什么是属性而不是函数NumPy 也可以选择不提供这些不同类型的索引方法或只通过特定函数而非上述属性记号来提供。但 NEP 认为新函数并非好的替代方案因为 Python 的[]索引记号在语法上有明显优势可以直接构造 slice 对象这是函数做不到的。一个更合理的替代方案是编写新的包装对象用函数而非方法实现替代索引例如np.oindex(arr)[indices]而非arr.oindex[indices]。功能上二者等价但索引是极其高频的操作NEP 认为最小化语法开销至关重要值得直接在ndarray对象上实现索引属性。索引属性还定义了一个清晰的接口方便其他数组实现照搬——这与当时的 NEP 18 — array function protocol 等让 NumPy 函数更易被覆写的努力相辅相成。6. 形状示例全集三种索引行为逐例对照NEP 21 的讨论部分给出了大量基于形状的示例所有原始维度都有 5 个或更多元素高级索引会插入更小的维度是理解三种索引语义的最佳材料。示例数组统一为 arr np.ones((5, 6, 7, 8))这些示例需要 NumPy 1.9 及以后的高级索引知识才能完全读懂。6.1 Legacy fancy indexing传统向量化索引注意同样的结果也可以用arr.legacy_index达成但标记为 future error 的情形在未来仍会报错。单个索引会被转置这对所有索引类型都一样 arr[[0], ...].shape (1, 6, 7, 8) arr[:, [0], ...].shape (5, 1, 7, 8)多个索引如果连续则被转置 arr[:, [0], [0], :].shape # future error (5, 1, 8) arr[:, [0], :, [0]].shape # future error (1, 5, 7)标量在此意义上也算整数数组索引并会与另一个高级索引广播 arr[:, [0], 0, :].shape (5, 1, 8) arr[:, [0], :, 0].shape # future error (scalar is fancy) (1, 5, 7)单个布尔索引可以作用于多个维度尤其是整个数组且必须匹配维度自 1.10 起维度不匹配会给出弃用警告。布尔索引与多个连续的整数数组索引等价 # Create boolean index with one True value for the last two dimensions: bindx np.zeros((7, 8), dtypenp.bool_) bindx[0, 0] True arr[:, 0, bindx].shape (5, 1) arr[0, :, bindx].shape (1, 6)与任何非标量组合时都令人困惑例如 arr[[0], :, bindx].shape # bindx result broadcasts with [0] (1, 6) arr[:, [0, 1], bindx].shape # IndexError6.2 Outer indexing外索引oindex多个索引是正交的其结果轴插入在原位彼此不广播 arr.oindex[:, [0], [0, 1], :].shape (5, 1, 2, 8) arr.oindex[:, [0], :, [0, 1]].shape (5, 1, 7, 2) arr.oindex[:, [0], 0, :].shape (5, 1, 8) arr.oindex[:, [0], :, 0].shape (5, 1, 7)布尔索引的结果总是插入在索引所在位置 bindx np.zeros((7, 8), dtypenp.bool_) bindx[0, 0] True arr.oindex[:, 0, bindx].shape (5, 1) arr.oindex[0, :, bindx].shape (6, 1)存在其它高级索引时行为不变 arr.oindex[[0], :, bindx].shape (1, 6, 1) arr.oindex[:, [0, 1], bindx].shape (5, 2, 1)6.3 Vectorized / inner indexing向量化索引vindex多个索引被广播并像 fancy 索引一样整体迭代但新轴总是插入在最前面 arr.vindex[:, [0], [0, 1], :].shape (2, 5, 8) arr.vindex[:, [0], :, [0, 1]].shape (2, 5, 7) arr.vindex[:, [0], 0, :].shape (1, 5, 8) arr.vindex[:, [0], :, 0].shape (1, 5, 7)布尔索引结果总是插入在索引所在位置与oindex完全一致因为布尔索引与其作用的轴紧密绑定 bindx np.zeros((7, 8), dtypenp.bool_) bindx[0, 0] True arr.vindex[:, 0, bindx].shape (5, 1) arr.vindex[0, :, bindx].shape (6, 1)但其它高级索引仍被转置到最前面 arr.vindex[[0], :, bindx].shape (1, 6, 1) arr.vindex[:, [0, 1], bindx].shape (2, 5, 1)7. 动机示例一个完整的数据分析场景设想一套数据采集软件记录D个通道沿时间轴的N个数据点存储为形状(N, D)的数组。分析时需要抓取一组通道比如计算它们的均值。先用随机数据模拟 arr np.random.random((100, 10))用户记得可以用整数数组索引于是写出正确代码 group arr[:, [2, 5]] mean_value group.mean()现在假设存在一些需要特别关注的时间点数据的第一维。这些时间点已知 interesting_times np.array([1, 5, 8, 10], dtypenp.intp)想抓取这些时间点的数据直接修改之前的代码就会撞上歧义 group_at_it arr[interesting_times, [2, 5]] IndexError: Ambiguous index, use .oindex or .vindex这样一个错误会引导用户去阅读索引文档使其明白oindex的行为更像切片因此在各种索引方法中显然是直觉之选此处实际报的是形状不匹配但错误信息完全可以顺带提示oindex group_at_it arr.oindex[interesting_times, [2, 5]]当然也可以改用vindex但要得到正确结果就远没那么直观了需要手动 reshape reshaped_times interesting_times[:, np.newaxis] group_at_it arr.vindex[reshaped_times, [2, 5]]接下来假设发现数据有些地方损坏需要把这些时间点的值替换为 0或其它值。第一列可能携带必要的信息于是利用布尔索引来修改 bad_data arr[:, 0] 0.5 arr[bad_data, :] 0 # (corrupts further examples)但列可能需要更细致地按组分别处理此时oindex属性就能优雅胜任 arr.oindex[bad_data, [2, 5]] 0注意用 legacy fancy indexing 完成这件事非常困难唯一途径是先构造整数数组 bad_data_indx np.nonzero(bad_data)[0] bad_data_indx_reshaped bad_data_indx[:, np.newaxis] arr[bad_data_indx_reshaped, [2, 5]]无论如何只用oindex就能在不触碰高级索引全部复杂性的情况下无歧义地完成所有这些操作。数据采集系统又加了新需求不同时间要用不同的传感器。假设已经构造了一个索引数组 correct_sensors np.random.randint(10, size(100, 2))它用(N, 2)的数组为每个时间列出两个正确传感器。第一反应可能是arr[:, correct_sensors]但这行不通——切片显然无法达成目标。此时用户会想起vindex这个更强大灵活的进阶索引工具。不过随手试arr.vindex[:, correct_sensors]可能会困惑它既不等价于期望结果也不是正确结果受转置规则影响因为切片在vindex中仍然保持原有语义。但只要阅读文档与示例就能迅速找到正确解法 rows np.arange(len(arr)) rows rows[:, np.newaxis] # make shape fit with correct_sensors new_arr arr.vindex[rows, correct_sensors]至此已离开oindex的直观世界进入了可随机挑选数组中任意元素的领域。值得一提的是rows不必是简单的arange可以是interesting_times interesting_times np.array([0, 4, 8, 9, 10]) correct_sensors_at_it correct_sensors[interesting_times, :] interesting_times_reshaped interesting_times[:, np.newaxis] new_arr_it arr[interesting_times_reshaped, correct_sensors_at_it]如果再把L次实验堆叠成形状(L, N, D)的数组情况会真正复杂起来。但对oindex而言不会出现意外vindex更强在这个场景下几乎必然会造成一些困惑但也能覆盖几乎所有需求。8. 与当前仓库的印证模拟工具与现状NEP 21 提出的oindex/vindex属性至今仍处于Deferred推迟状态尚未在 NumPy 主分支实现。不过提案所依赖的两类脚手架在当前仓库中都可以找到具体实现外索引的模拟工具np.ix_定义在 numpy/lib/_index_tricks_impl.py。它接收 N 个一维序列返回 N 个 N 维数组组成的开放网格每个数组只有一个维度非 1从而把外索引转换为向量化索引。其 docstring 明确给出等价关系a[np.ix_([1,3],[2,5])]返回[[a[1,2] a[1,5]], [a[3,2] a[3,5]]]布尔序列会被解释为对应维度的布尔掩码等价于传入np.nonzero(boolean_sequence)非一维输入会抛出ValueError(Cross index must be 1 dimensional)。这正是 NEP 21 所说外索引总可用向量化索引模拟的具体落点开放网格ogrid/ 密集网格mgrid同为 numpy/lib/_index_tricks_impl.py 中nd_grid类的两个预置实例mgrid nd_grid(sparseFalse)、ogrid nd_grid(sparseTrue)。ogrid返回的稀疏网格配合ix_一样是构造外索引广播形状的常用手段。此外当前官方文档 doc/source/user/basics.indexing.rst 中的索引章节正是 legacy indexing 规则的权威说明——其中明确写道基本索引之外存在两种高级索引整数与布尔高级索引意味着x[(1, 2, 3),]这类写法会触发高级索引路径、结果总是副本且当多个高级索引出现时会涉及复杂的结果子空间放置规则连续的高级索引轴放在一起否则放在最前面。读者可以对照 NEP 21 的示例与这篇官方文档观察 legacy 规则的惊喜点正是 NEP 21 试图消除的部分。NEP 21 还引用了 NEP 18 — array function protocol 作为可借鉴的、让数组实现更易覆写 NumPy 函数行为的先例为索引属性作为外部库可复制的接口这一论点提供支撑。关于该提案的社区讨论最初发源于 NumPy 邮件列表2015 年 4 月后续讨论散见于当时的 PR 与 Python 参考实现中NEP 21 自身同时提供了一份 CC0 1.0 Universal (CC0 1.0) Public Domain Dedication 的版权声明。9. 总结NEP 21 是一份面向下一代索引体验的完整设计蓝图它精确诊断了 legacy advanced indexing 的三个痛点外索引缺失、向量化索引转置规则反直觉、混合索引边界情况复杂并给出了一套自洽的解决方案——用oindex表达正交外索引用vindex表达无转置的向量化索引用legacy_index承接存量代码再逐步弃用歧义的普通索引写法。虽然该提案目前仍处于 Deferred 状态、尚未落地为主分支特性但它对索引语义的剖析product 与 zip 两种心智模型的冲突、布尔索引与外索引的亲缘关系、连续高级索引的转置规则至今仍是理解 NumPy 索引行为的最佳教材也是 dask、xarray 等生态库设计兼容索引接口时反复参照的坐标系。对于每一位想彻底掌握 NumPy 索引的用户先读懂 NEP 21 的示例再回到 官方索引文档 对照练习是最短的学习路径。赞分享科学计算数据分析【免费下载链接】numpyThe fundamental package for scientific computing with Python.项目地址https://gitcode.com/gh_mirrors/nu/numpy点击查看免费下载相关推荐NumPy ufunc 可扩展性重构NEP 43 与 ArrayMethod 架构深度解析NumPy ufunc 可扩展性重构NEP 43 与 ArrayMethod 架构深度解析 导读 本文基于 NumPy 官方设计文档 NEP 43 — Enh科学计算数据分析ip2region向量索引vIndex缓存机制深度解析ip2region向量索引vIndex缓存机制深度解析 引言IP定位的性能瓶颈与突破 在当今互联网应用中IP地址定位是许多业务场景的核心需求从地理位置服后端网络NumPy NEP 49 深度解析用 PyDataMem_Handler 自定义 ndarray 数据内存分配策略NumPy NEP 49 深度解析用 PyDataMem_Handler 自定义 ndarray 数据内存分配策略 本文基于 NumPy 官方 NEP 49科学计算数据分析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价