在什么情况下,Python3 Numpy会给我一个引用,而不是一个副本?

whhtz7ly  于 2023-08-05  发布在  Python
关注(0)|答案(3)|浏览(88)

昨天我在使用numpy matrices,我遇到了一个让我加倍考虑的行为,具体来说:

>>> X=np.array([[1,5],[2,4],[3,6]])
>>> X
array([[1, 5],
       [2, 4],
       [3, 6]]) # Perfecto
>>> a=X[:,0]
>>> a
array([1, 2, 3])  # Exactly
>>> X
array([[1, 5],
       [2, 4],
       [3, 6]]) #Yup
>>> a[0]=999
>>> a
array([999,   2,   3]) # :)  
>>> X
array([[999,   5],
       [  2,   4],
       [  3,   6]]) # Errrrr, Huh?  
#OK, Fine.
>>> b=np.matrix(X[:,1])
>>> b
matrix([[5, 4, 6]])
>>> b[0]=666
>>> X
array([[1, 5],
       [2, 4],
       [3, 6]]) # That's what I wanted

字符串
我最初期望Python会隐式地假定我的数据类型是np.matrix,并调用一个复制构造函数。我找到了现有母体的参考资料。作为一个C程序员,我讨厌引用变量。我宁愿得到一个内存位置,并在代码中显式地DE-REFERENCE内存位置并转换赋值。但是美丽新世界需要开放的思想我想这是Numpy的细微差别。所以我问,在Python库中,当我应该期望在赋值时获得引用时,是否有经验法则?那个意想不到的结果比猴子的腋窝还难看

8fsztsew

8fsztsew1#

我不明白C和推荐人的区别这并不比你在C中使用的引用更多(此外,它碰巧是C,因为这是numpy)。我是C程序员。我讨厌引用(我的意思是,C++的引用)。我想传递一个指向变量的指针,这个指针可以被修改。但这不是一个。
在C中

int arr[9]={1,2,3,4,5,6,7,8,9}; 
int *subarr=arr+3; 
subarr[0]=99; 
printf("%d\n", arr[3]);

字符串
打印99这就是你在这里做的榜样。提取一个子数组。
该数组包含一个指向相同数据的指针
就像我说的,不是只有你才能用C语言编写这样的代码。但实际上它是用C编写的。在C中,数组也是指针。当你把一个数组arr传递给一个函数时,即使arr本身是通过值传递的,也就是说这个函数不能修改arr本身(就像你在例子中修改X一样),这个函数仍然可以修改arr的内容。就像你在这里改变了X的内容一样。真的,没有区别。您没有对X的引用。但是,子数组仍然共享指向X结构内部的数据。
至于你的确切问题,严格的答案是“这取决于,但它是在文档中说”。但经验法则是“当它可以时,它通常会共享数据”(您称之为“hand a reference”)。要猜测它什么时候可以,你必须理解什么是numpy数组:它是一堆连续数据、一个形状、一个初始指针、一个数据类型和步幅。所以索引为[i1,i2,...,in]的元素存在当且仅当对所有k都是ik<shapeₖ,并且该元素的地址为data+stride₁×i₁+stride₂×i₂+…+stridesₖ×iₖ
通常(经验法则),如果可以通过构建一组新的步幅/形状来完成操作,但使用相同的数据,则以这种方式完成,而无需复制数据。
例如X[:,1]:假设数据位于地址Xaddr,形状为(shapeX1, shapeX2),跨距为(strideX1, strideX2),则地址为Yaddr=Xaddr+1×strideX2,形状为(shapeX1,),跨距为(strideX1,)的阵列YX的第二列。因此,由于可以这样做来计算X[:,1],因此可以避免复制(同样,这不是一个严格的规则,只是一个经验)。
同样,X.T(转置)是地址为Xaddr的数组,其形状为(shapeX2,shapeX1),步幅为(strideX2,strideX1)。所以,不需要复制,所以它不是。所以呢

X=np.arange(12).reshape(4,3)
X.T[0,0]=99
X


展示

array([[99,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])


另一方面,复杂索引生成副本。

X=np.arange(12).reshape(4,3)
X[[0,2,3], :][0,0]=99
X


展示

array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])


因为你不能通过调整步幅和形状来做高级索引。
注意,X[::2,:][0,0]=99确实改变了X。当X[[0,2],:][0,0]没有。然而X[::2,:]X[[0,2],:]的意思是相同的(只保留偶数行)。但是,第一个总是可行的,只有步幅调整,当第二个碰巧只有这个数字,但不会如果线选择不均匀选择(如在我以前的[0,2,3]例子)。
这条经验法则甚至适用于非常复杂的案件。
比如说

X=np.arange(12).reshape(4,3)
Y=X.reshape(6,2)
Y[0,0]=111
print(X[0,0]) # 111
Z=X[:2,:2]
Z[0,0]=222
print(X[0,0]) # 222
T=Z.reshape(1,4)
T[0,0]=333
print(X[0,0], Y[0,0], Z[0,0]) # 222 222 222, not 333


因此,第一次重塑(Y)是“视图”,而第二次重塑(T)是副本。
为什么,因为X数据是连续的。有点像在C语言中,你是这样实现的

int *x=malloc(12);
for(int i=0;i<12;i++) x[i]=i;
int xstride1=sizeof(int)*3, xstride2=sizeof(int);
// x[i,j] is *(int *)((char *)x+xstride1*i+xstride2*j)

int *y=x; // same 12 consecutive numbers
int ystride1=sizeof(int)*2, ystride2=sizeof(int);
// y[i,j] is *(int *)((char *)y+ystride1*i+ystride2*j)

int *z=x;
int zstride1=sizeof(int)*3, zstride2=sizeof(int);
// z[i,j] is *(int *)((char *)z+zstride1*i+zstride2*j)
// Some exactly same as x. Normal: we only changed shape.

// But you can't do anything like
int *t=x;
int tstride1=???, int stride2=???;
// t[i,j] is *(int *)((char *)t+i*tstride1+j*tstride2)()


没有任何tstride1tstride2是真的。只需观察t值:它们是从0,1,2,3,4,5,6,7,8,9,10,11中提取的值0、1、3、4。它们在初始数据数组中不是均匀分布的。因此,没有一个唯一的tstrid2来告诉有多少字节将0与1、1与3、3与4分开。因此,不可能在不构建新数据集的情况下构建T
这就是为什么操作.reshape(...)有时返回一个视图,有时返回一个副本。
所有这些都在文档中说明。某些操作明确返回一个副本。有些人明确地返回一个视图。而对于一些文档来说,“你不能假设它是一个视图还是一个副本”。因为这取决于实现检测是否可以重用初始数据的智能程度。

vybvopom

vybvopom2#

https://numpy.org/doc/stable/user/basics.copies.html
您的阵列:

In [116]: X=np.array([[1,5],[2,4],[3,6]])
In [117]: X
Out[117]: 
array([[1, 5],
       [2, 4],
       [3, 6]])
In [118]: a=X[:,0]
In [119]: a
Out[119]: array([1, 2, 3])

字符串
这是basic indexing,所以aview。检查其base

In [120]: a.base
Out[120]: 
array([[1, 5],
       [2, 4],
       [3, 6]])


有关阵列的更多信息:

In [123]: X.__array_interface__
Out[123]: 
{'data': (94507967116736, False),
 'strides': None,
 'descr': [('', '<i8')],
 'typestr': '<i8',
 'shape': (3, 2),
 'version': 3}
In [124]: a.__array_interface__
Out[124]: 
{'data': (94507967116736, False),
 'strides': (16,),
 'descr': [('', '<i8')],
 'typestr': '<i8',
 'shape': (3,),
 'version': 3}


请注意相同的data数字。这是一个指向数组(一个c数组)的数据缓冲区的指针。阅读更多numpy基础知识,了解如何存储数组。
您的矩阵是一个副本,具有自己的基础:

In [125]: m=np.matrix(X)
In [126]: m.base


矩阵的视图版本:

In [127]: m=np.asmatrix(X)
In [128]: m.base
Out[128]: 
array([[1, 5],
       [2, 4],
       [3, 6]])


您还可以指定copy以确保新数组是它自己的基:

In [129]: a.copy().base


advanced indexing生成一个副本:

In [130]: b = X[:,[0]]
In [131]: b
Out[131]: 
array([[1],
       [2],
       [3]])


注意a的不同形状,(3,1)而不是(3,)。

In [132]: b.base
Out[132]: array([[1, 2, 3]])


检查基数可以发现索引的细微差别。这个基数是(1,3)数组,不是Xa。索引步骤实际上创建了这个(1,3)数组,并通过返回其转置(视图创建步骤)完成了它。

w8biq8rn

w8biq8rn3#

也许你应该使用'copy'来获得一个新的独立对象。我希望这就是你要找的。

b = np.matrix(X[:,1]).copy()

字符串

相关问题