numpy:整数索引与布尔索引的值默认复制问题
我最近开始学习麦金尼的《Python数据分析》。书中有一段让我困惑:
数组切片是对原始数组的视图。这意味着数据不会被复制,对视图的任何修改都会反映在源数组中……因为NumPy是为了处理大数据而设计的,如果NumPy总是从左到右复制数据,可能会导致性能和内存问题。
这听起来不错,似乎是个合理的设计选择。但两页后又说:
通过布尔索引从数组中选择数据总是会创建数据的副本,即使返回的数组没有变化。
等等,什么情况?还有,
你甚至可以将布尔数组和切片混合使用……例如
data[names == 'Bob', 2:]
那这会返回什么?是数据副本的视图吗?为什么会有这样的行为?我来自R语言,看到布尔索引和基于位置的索引都很常用。如果NumPy是为了避免复制内存而设计的,那这个设计选择背后的原因是什么呢?
谢谢。
2 个回答
需要复制,因为使用任意的布尔索引时,你不能保证结果能被表示成一个ndarray(多维数组)。
详细信息请查看:http://scipy-lectures.github.io/advanced/advanced_numpy/#indexing-scheme-strides
切片返回的是视图,因为
任何东西都可以通过仅仅改变
shape
(形状)、strides
(步幅),并可能调整data
指针来表示!
假设我们有一个一维数组。在内存中的数据大概是这样的:
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每次都得等着取数据。此时,做一次连续的复制会更快,这样就可以直接使用这份数据。