我是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。分裂-我错过了什么?
2条答案
按热度按时间ttcibm8c1#
您看到的行为只是与
split
松散相关。numpy
数组后面有一个"矩形块",它们不能引用多个独立的分配。(通过跨步、掩蔽、转置等),因此各个条目不一定是"连续的",但是它们都由一个支持,并且只有一个,连续大容量分配。这种分配对于数组或者其他数组的视图可以是唯一的,但是一个给定的数组对象在拥有存储和查看存储之间不会改变,它是一个或另一个。数组绑定到的 * name * 可以被重新分配,但是所有的名称都可以被重新分配(
x = 1
并不使x
总是特定的int
,或者甚至总是 * some *int
;x = ANYTHING
稍后会将其重新绑定到任意类型的全新对象,忽略它以前的样子)。了解了这一点,就可以清楚地知道,将一个数组的"部分"重新分配为其他数组的视图是"不可能的",下面我们来解释一下你的各种观察结果:
正如您所注意到的,这是预期的行为。
np.split
返回了一个list
,其中包含三个视图类型的新数组,每个数组都由x
的一部分支持。这里的第1行创建了一个 * owning * 类型的新数组。即使可以用不同的存储缓冲区替换底层的存储缓冲区(或者拥有一个新的缓冲区,或者查看其他数组的缓冲区),我不保证这是不可能的(我对
numpy
的研究还不够深入,无法确定),所以绝对不可能将拥有和查看混合在一起。如果这段代码可以工作,并且 * 没有 * 将数据从
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
* 已经 * 是x
的视图,但是副本仍在制作中;x2
已经是x[:3]
的一个视图,您刚刚告诉numpy
将x[:3]
的内容复制到x2[:]
。在底层,一旦使用完整切片和另一个numpy
视图的参数调用x2.__setitem__
,并且numpy
具有足够的信息和控制,它 * 可能 * 会注意到原始内存地址是相同的,并避免复制,但如果它只是盲目地将视图中每个地址的数据复制到自己身上,我一点也不会感到惊讶。如果您没有重用
x2
到x4
,您将能够看到它没有创建新视图:这里的简短版本是:
1.* * 将 * anything * 赋值给普通名称**(无索引/分片)将重新绑定该名称,而不复制数据(忽略任何曾经绑定到该名称的内容);如果你分配了一个查看数组,它现在就是一个视图,如果你分配了一个拥有数组,它现在就是一个拥有数组。
1.* * 将视图或所有权数组分配给现有数组的索引或切片**(视图或所有权)会将数据从一个数组复制到另一个数组(如果适用,复制到已查看缓冲区)
这就是Python中所有类型的工作方式,
numpy
的唯一不寻常之处是你可以 * make * 视图;内置的Python序列(除了有点像numpy
的奇怪的memoryview
)没有视图的概念,所以等号右边的切片将是浅拷贝,但是左边的赋值 * to * 切片仍然会拷贝(不管右边是视图还是拷贝)。cvxl0en22#
在ipython会话中,创建
x
并拆分检查一项:
我发现下面显示的数组信息很有用。“data”字段“指向”底层的数据缓冲区。它不能在代码中使用,但数字是一个有用的标识符,可以标识值实际存储的位置。
x2
具有相同的“数据”值:x3
和x4
将具有稍微不同的值,指向缓冲器中更远的字节(例如,2389644026236
指向x3
,3*4更远)。x5
是一个新数组,具有自己的数据缓冲区:为
x5
赋值会复制这些值,但不会更改其数据缓冲区位置:x
和x5
仍然是它们自己的“基础”:我们可以索引
x
中的另外3个值,并将它们分配给x2
视图:x2
将被更改,但其基也将更改:[39]中的赋值和将3个值复制到
x5
一样“昂贵”(时间方面),它仍然是复制,但是不同之处也在于值被复制的位置。