numpy:整数索引与布尔索引的值默认复制问题

8 投票
2 回答
681 浏览
提问于 2025-04-18 04:04

我最近开始学习麦金尼的《Python数据分析》。书中有一段让我困惑:

数组切片是对原始数组的视图。这意味着数据不会被复制,对视图的任何修改都会反映在源数组中……因为NumPy是为了处理大数据而设计的,如果NumPy总是从左到右复制数据,可能会导致性能和内存问题。

这听起来不错,似乎是个合理的设计选择。但两页后又说:

通过布尔索引从数组中选择数据总是会创建数据的副本,即使返回的数组没有变化。

等等,什么情况?还有,

你甚至可以将布尔数组和切片混合使用……例如 data[names == 'Bob', 2:]

那这会返回什么?是数据副本的视图吗?为什么会有这样的行为?我来自R语言,看到布尔索引和基于位置的索引都很常用。如果NumPy是为了避免复制内存而设计的,那这个设计选择背后的原因是什么呢?

谢谢。

2 个回答

2

需要复制,因为使用任意的布尔索引时,你不能保证结果能被表示成一个ndarray(多维数组)。

详细信息请查看:http://scipy-lectures.github.io/advanced/advanced_numpy/#indexing-scheme-strides

切片返回的是视图,因为

任何东西都可以通过仅仅改变 shape(形状)、strides(步幅),并可能调整 data 指针来表示!

8

假设我们有一个一维数组。在内存中的数据大概是这样的:

10 | 11 | 12 | 13 | 14 | 15 | 16

通过索引访问数组中的元素非常简单。只需要找到第一个元素的位置,然后向后跳 n 步就可以了。所以,对于 arr[2]

10 | 11 | 12 | 13 | 14 | 15 | 16
           ^

我只需要做一次乘法就能找到在内存中的位置。这既快又简单。

我还可以做一个切片,比如说“只取 arr2 = arr[2:-1]”:

10 | 11 | 12 | 13 | 14 | 15 | 16
           ^----^----^----^

这时,内存的布局非常相似。获取一个元素也只需要从新的起始点做一次乘法。比如 arr2[1]

10 | 11 | 12 | 13 | 14 | 15 | 16
  (ignore) -----^----------

你还可以做一个更高级的操作,比如说 arr3 = arr[::2],这样就可以每隔一个元素取一个。

10 | 11 | 12 | 13 | 14 | 15 | 16
 ^---------^---------^---------^

同样,获取 arr3 的索引也很简单:只需做一次乘法,不过这次的大小变大了。这就是步幅的作用,它告诉你每个块的大小以及如何通过索引获取元素。步幅在多维数组中更加强大。顺便说一下,这也是我们如何将一维内存转换成二维矩阵的方法。

接下来,我们来聊聊布尔数组。如果我的掩码是:T F T T F F T,当我问你第三个元素时,你需要遍历这个掩码,找到第三个“真”,然后获取它的索引;这样做会非常慢。因此,当使用布尔掩码时,我们必须复制数据。有些掩码可以用步幅表示,但一般来说,为了保持一致性,还是要复制数据。

顺便提一下,有时候,复制数据的成本在性能上是值得的。如果你想进行很多操作,比如读取“数组中的每第五个元素”,那么内存中的数据可能不会对齐,这样CPU每次都得等着取数据。此时,做一次连续的复制会更快,这样就可以直接使用这份数据。

撰写回答