numpy 为什么np.split在传入现有数组时会创建一个副本?

sd2nnvve  于 2023-02-04  发布在  其他
关注(0)|答案(2)|浏览(122)

我是Python新手,正在尝试理解视图和副本的行为,所以如果这是一个显而易见的问题,请道歉!在下面的例子中,我使用np.split()来拆分一个数组x。(x1,3个1D数组的列表,或x2, x3, x4,三个单独的1D数组)对象是x的视图,如预期:

import numpy as np

x = np.arange(1, 10)                        # create an array of length 9

x1 = np.split(x, (3,6))                     # split array into 1 new object (list of arrays)
print(x1[0].base)                           # each array in list is a view

x2, x3, x4 = np.split(x, (3, 6))            # split array into 3 new objects
print(x2.base)                              # these objects are views

但是,如果我创建一个空的(3,3)数组x5,并将np.split传入该数组的每一行(我知道这是一件愚蠢的事情,我只是想弄清楚拆分是如何工作的),则会创建一个副本:

x5 = np.empty((3,3), dtype = np.int32)      # create an uninitialised array
x5[0], x5[1], x5[2] = np.split(x, (3, 6))   # split x into each row of x5
print(x5.base)                              # this object is a COPY

我想可能是x5的切片导致了拷贝的产生,但是如果我对x2, x3, x4进行切片,它们仍然是视图:

x2[:], x3[:], x4[:] = np.split(x, (3, 6))   # split array into 3 existing objects using indexing
print(x2.base)                              # these objects are views

我还没有设法找到一个解释,这在任何解释的意见和副本或np。分裂-我错过了什么?

ttcibm8c

ttcibm8c1#

您看到的行为只是与split松散相关。numpy数组后面有一个"矩形块",它们不能引用多个独立的分配。(通过跨步、掩蔽、转置等),因此各个条目不一定是"连续的",但是它们都由一个支持,并且只有一个,连续大容量分配。
这种分配对于数组或者其他数组的视图可以是唯一的,但是一个给定的数组对象在拥有存储和查看存储之间不会改变,它是一个或另一个。数组绑定到的 * name * 可以被重新分配,但是所有的名称都可以被重新分配(x = 1并不使x总是特定的int,或者甚至总是 * some * int;x = ANYTHING稍后会将其重新绑定到任意类型的全新对象,忽略它以前的样子)。
了解了这一点,就可以清楚地知道,将一个数组的"部分"重新分配为其他数组的视图是"不可能的",下面我们来解释一下你的各种观察结果:

x2, x3, x4 = np.split(x, (3, 6))            # split array into 3 new objects
print(x2.base)                              # these objects are views

正如您所注意到的,这是预期的行为。np.split返回了一个list,其中包含三个视图类型的新数组,每个数组都由x的一部分支持。

x5 = np.empty((3,3), dtype = np.int32)      # create an uninitialised array
x5[0], x5[1], x5[2] = np.split(x, (3, 6))   # split x into each row of x5
print(x5.base)                              # this object is a COPY

这里的第1行创建了一个 * owning * 类型的新数组。即使可以用不同的存储缓冲区替换底层的存储缓冲区(或者拥有一个新的缓冲区,或者查看其他数组的缓冲区),我不保证这是不可能的(我对numpy的研究还不够深入,无法确定),所以绝对不可能将拥有和查看混合在一起。

x5[0] = x[:3]

如果这段代码可以工作,并且 * 没有 * 将数据从x的视图复制到x5[0]的视图,那么x5[0]就必须是x的视图,而x5的其余数据将被拥有。缓冲区中支持x5[0]的三个元素会发生什么变化?您可能会想"但是我"我一次将它们全部替换掉",但实际上并不是这样。x5[0], x5[1], x5[2] = np.split(x, (3, 6))实际上等效于__unnamedtemp = [x[:3], x[3:6], x[6:]],然后是x5[0] = __unnamedtemp[0],然后是x5[1] = __unnamedtemp[1],然后是x5[2] = __unnamedtemp[2]。(解包是Python的一个通用特性,numpy不能挂钩),所以即使在理论上,最终结果可以让x5查看x,但实际上它不能,即使numpy想这样做,因为中间阶段是非法状态。
相比之下

x2[:], x3[:], x4[:] = np.split(x, (3, 6))   # split array into 3 existing objects using indexing
print(x2.base)

"工作"仅仅是因为x2x3x4 * 已经 * 是x的视图,但是副本仍在制作中;x2已经是x[:3]的一个视图,您刚刚告诉numpyx[:3]的内容复制到x2[:]。在底层,一旦使用完整切片和另一个numpy视图的参数调用x2.__setitem__,并且numpy具有足够的信息和控制,它 * 可能 * 会注意到原始内存地址是相同的,并避免复制,但如果它只是盲目地将视图中每个地址的数据复制到自己身上,我一点也不会感到惊讶。
如果您没有重用x2x4,您将能够看到它没有创建新视图:

x2, x3, x4 = np.arange(1, 4), np.arange(1, 4), np.arange(1, 4)  # Three owning arrays
x2[:], x3[:], x4[:] = np.split(x, (3, 6))  # Makes three views, then copies from views to owned buffers
print(x2.base)  # Does not have a base, because it's still not a view

这里的简短版本是:
1.* * 将 * anything * 赋值给普通名称**(无索引/分片)将重新绑定该名称,而不复制数据(忽略任何曾经绑定到该名称的内容);如果你分配了一个查看数组,它现在就是一个视图,如果你分配了一个拥有数组,它现在就是一个拥有数组。
1.* * 将视图或所有权数组分配给现有数组的索引或切片**(视图或所有权)会将数据从一个数组复制到另一个数组(如果适用,复制到已查看缓冲区)
这就是Python中所有类型的工作方式,numpy的唯一不寻常之处是你可以 * make * 视图;内置的Python序列(除了有点像numpy的奇怪的memoryview)没有视图的概念,所以等号右边的切片将是浅拷贝,但是左边的赋值 * to * 切片仍然会拷贝(不管右边是视图还是拷贝)。

cvxl0en2

cvxl0en22#

在ipython会话中,创建x并拆分

In [23]: x=np.arange(1,10);x
Out[23]: array([1, 2, 3, 4, 5, 6, 7, 8, 9])
In [24]: x2,x3,x4 = np.split(x,(3,6))

检查一项:

In [25]: x2
Out[25]: array([1, 2, 3])    
In [26]: x2.base
Out[26]: array([1, 2, 3, 4, 5, 6, 7, 8, 9])

我发现下面显示的数组信息很有用。“data”字段“指向”底层的数据缓冲区。它不能在代码中使用,但数字是一个有用的标识符,可以标识值实际存储的位置。

In [28]: x.__array_interface__
Out[28]: 
{'data': (2389644026224, False),
 'strides': None,
 'descr': [('', '<i4')],
 'typestr': '<i4',
 'shape': (9,),
 'version': 3}

x2具有相同的“数据”值:

In [29]: x2.__array_interface__['data']
Out[29]: (2389644026224, False)

x3x4将具有稍微不同的值,指向缓冲器中更远的字节(例如,2389644026236指向x3,3*4更远)。
x5是一个新数组,具有自己的数据缓冲区:

In [30]: x5 = np.empty((3,3),int);x5
Out[30]: 
array([[4128860, 6029375, 3801155],
       [5570652, 6619251, 7536754],
       [7340124, 7667809,     108]])    
In [31]: x5.__array_interface__['data']
Out[31]: (2389645593712, False)

x5赋值会复制这些值,但不会更改其数据缓冲区位置:

In [32]: x5[:] = x2,x3,x4    
In [33]: x5
Out[33]: 
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])    
In [34]: x5.__array_interface__['data']
Out[34]: (2389645593712, False)

xx5仍然是它们自己的“基础”:

In [35]: x.base, x5.base
Out[35]: (None, None)

我们可以索引x中的另外3个值,并将它们分配给x2视图:

In [38]: x[-2:-5:-1]
Out[38]: array([8, 7, 6])    
In [39]: x2[:]=x[-2:-5:-1]      # not x2=x[...]

x2将被更改,但其基也将更改:

In [40]: x2.base
Out[40]: array([8, 7, 6, 4, 5, 6, 7, 8, 9])    
In [41]: x
Out[41]: array([8, 7, 6, 4, 5, 6, 7, 8, 9])

[39]中的赋值和将3个值复制到x5一样“昂贵”(时间方面),它仍然是复制,但是不同之处也在于值被复制的位置。

相关问题