python 当使用reshape()时,numpy何时复制数组

bbuxkriu  于 2023-10-14  发布在  Python
关注(0)|答案(3)|浏览(94)

numpy.reshape的文档中,它说:
如果可能,这将是一个新的视图对象;否则,它将是一个副本。请注意,无法保证返回数组的内存布局(C或Fortran连续)。
我的问题是,numpy什么时候会选择返回一个新视图,什么时候复制整个数组?有没有什么一般性的原则告诉人们reshape的行为,或者它只是不可预测的?

xienkqul

xienkqul1#

@mgillson发现的链接似乎解决了“我如何判断它是否复制”的问题,但没有解决“我如何预测它”或理解它为什么复制。至于测试,我喜欢使用A.__array_interfrace__
如果您试图将值赋给整形后的数组,并希望同时更改原始数组,则很可能会出现问题。我很难找到一个这样的情况下,这是问题。
复制的重塑会比非复制的重塑慢一点,但我还是想不出会导致整个代码变慢的情况。如果您使用的数组太大,以至于最简单的操作都会产生内存错误,那么副本也可能是一个问题。
在整形之后,数据缓冲区中的值需要以连续的顺序,“C”或“F”。举例来说:

In [403]: np.arange(12).reshape(3,4,order='C')
Out[403]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

In [404]: np.arange(12).reshape(3,4,order='F')
Out[404]: 
array([[ 0,  3,  6,  9],
       [ 1,  4,  7, 10],
       [ 2,  5,  8, 11]])

如果初始顺序太“混乱”以至于不能返回这样的值,它会做一个复制。转置后的整形可以做到这一点(见我下面的例子)。stride_tricks.as_strided的游戏也是如此。这是我能想到的唯一案例。

In [405]: x=np.arange(12).reshape(3,4,order='C')

In [406]: y=x.T

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

In [408]: y.__array_interface__
Out[408]: 
{'version': 3,
 'descr': [('', '<i4')],
 'strides': (4, 16),
 'typestr': '<i4',
 'shape': (4, 3),
 'data': (175066576, False)}

y,转置,具有相同的“data”指针。执行转置时没有更改或复制数据,它只是用新的shapestridesflags创建了一个新对象。

In [409]: y.flags
Out[409]: 
  C_CONTIGUOUS : False
  F_CONTIGUOUS : True
  ...

In [410]: x.flags
Out[410]: 
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  ...

y是F阶。现在试着重塑它

In [411]: y.shape
Out[411]: (4, 3)

In [412]: z=y.reshape(3,4)

In [413]: z.__array_interface__
Out[413]: 
{...
 'shape': (3, 4),
 'data': (176079064, False)}

In [414]: z
Out[414]: 
array([[ 0,  4,  8,  1],
       [ 5,  9,  2,  6],
       [10,  3,  7, 11]])

z是一个副本,它的data缓冲区指针不同。它的值不以任何类似于xy的方式排列,没有0,1,2,...
但是简单地重塑x并不能产生副本:

In [416]: w=x.reshape(4,3)

In [417]: w
Out[417]: 
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])

In [418]: w.__array_interface__
Out[418]: 
{...
 'shape': (4, 3),
 'data': (175066576, False)}

Raveling yy.reshape(-1)相同;它产生的副本:

In [425]: y.reshape(-1)
Out[425]: array([ 0,  4,  8,  1,  5,  9,  2,  6, 10,  3,  7, 11])

In [426]: y.ravel().__array_interface__['data']
Out[426]: (175352024, False)

将值复制到像这样的松散数组中可能是复制最有可能产生错误的情况。例如,x.ravel()[::2]=99每隔一个xy的值(分别为列和行)就更改一次。但是y.ravel()[::2]=0由于这种复制而什么也不做。
因此,转置后的整形是最有可能的复制场景。我很乐意探索其他可能性。

edit:y.reshape(-1,order='F')[::2]=0会更改y的值。使用兼容顺序时,整形不会生成副本。

在@mgillson的链接https://stackoverflow.com/a/14271298/901925中有一个答案,指出A.shape=...语法阻止了复制。如果它不能在不复制的情况下改变形状,它将引发一个错误:

In [441]: y.shape=(3,4)
...
AttributeError: incompatible shape for a non-contiguous array

reshape文档中也提到了这一点
如果您希望在复制数据时引发错误,则应将新形状分配给数组的shape属性::
所以关于重塑以下as_strided的问题:
reshaping a view of a n-dimensional array without using reshape

Numpy View Reshape Without Copy (2d Moving/Sliding Window, Strides, Masked Memory Structures)

这是我将shape.c/_attempt_nocopy_reshape翻译成Python的第一步。它可以运行如下内容:

newstrides = attempt_reshape(numpy.zeros((3,4)), (4,3), False)
import numpy   # there's an np variable in the code
def attempt_reshape(self, newdims, is_f_order):
    newnd = len(newdims)
    newstrides = numpy.zeros(newnd+1).tolist()  # +1 is a fudge

    self = numpy.squeeze(self)
    olddims = self.shape
    oldnd = self.ndim
    oldstrides = self.strides

    #/* oi to oj and ni to nj give the axis ranges currently worked with */

    oi,oj = 0,1
    ni,nj = 0,1
    while (ni < newnd) and (oi < oldnd):
        print(oi, ni)
        np = newdims[ni];
        op = olddims[oi];

        while (np != op):
            if (np < op):
                # /* Misses trailing 1s, these are handled later */
                np *= newdims[nj];
                nj += 1
            else:
                op *= olddims[oj];
                oj += 1

        print(ni,oi,np,op,nj,oj)

        #/* Check whether the original axes can be combined */
        for ok in range(oi, oj-1):
            if (is_f_order) :
                if (oldstrides[ok+1] != olddims[ok]*oldstrides[ok]):
                    # /* not contiguous enough */
                    return 0;
            else:
                #/* C order */
                if (oldstrides[ok] != olddims[ok+1]*oldstrides[ok+1]) :
                    #/* not contiguous enough */
                    return 0;

        # /* Calculate new strides for all axes currently worked with */
        if (is_f_order) :
            newstrides[ni] = oldstrides[oi];
            for nk in range(ni+1,nj):
                newstrides[nk] = newstrides[nk - 1]*newdims[nk - 1];
        else:
            #/* C order */
            newstrides[nj - 1] = oldstrides[oj - 1];
            #for (nk = nj - 1; nk > ni; nk--) {
            for nk in range(nj-1, ni, -1):
                newstrides[nk - 1] = newstrides[nk]*newdims[nk];
        nj += 1; ni = nj
        oj += 1; oi = oj  
        print(olddims, newdims)  
        print(oldstrides, newstrides)

    # * Set strides corresponding to trailing 1s of the new shape.
    if (ni >= 1) :
        print(newstrides, ni)
        last_stride = newstrides[ni - 1];
    else :
        last_stride = self.itemsize # PyArray_ITEMSIZE(self);

    if (is_f_order) :
        last_stride *= newdims[ni - 1];

    for nk in range(ni, newnd):
        newstrides[nk] = last_stride;
    return newstrides
uxh89sit

uxh89sit2#

你可以通过只测试相关维度的连续性来预测。
(Here是numpy决定使用视图还是副本的代码。
邻接性意味着任何维度的 * 步幅 * 都恰好等于下一个变化更快的维度的 * 步幅×长度 *。
例如,“涉及”意味着,如果最里面的维度和最外面的维度不连续,只要保持它们相同,就不应该有关系。
一般来说,如果您所做的只是重新塑造整个数组,那么您可以期待一个视图。如果你正在处理一个更大的数组的子集,或者以任何方式重新排序了元素,那么很可能会有一个副本。
例如,考虑一个矩阵:

A = np.asarray([[1,2,3],
                [4,5,6]], dtype=np.uint8)

底层数据(假设我们要将数组分解为一维)已经作为[1, 2, 3, 4, 5, 6]存储在内存中。数组的形状为(2, 3),步幅为(3, 1)。您可以通过交换维度的步幅(以及长度)来转置它。所以在A.T中,内存中前进3个元素将把你放在一个新列而不是(像以前一样)一个新行。

[[1, 4],
 [2, 5],
 [3, 6]]

如果我们想解开转置(即,将A.T整形为长度为6的一维数组),那么我们期望结果为[1 4 2 5 3 6]。然而,没有步幅允许我们在这个特定序列中遍历原始存储序列中的所有6个元素。因此,虽然A.T是一个视图,但A.T.ravel()将是一个副本(可以通过检查它们各自的.ctypes.data属性来确认)。

bzzcjhmw

bzzcjhmw3#

@hoaulj提供了一个很好的答案,但在他实现_attempt_nocopy_reshape函数时有一个错误。如果读者注意到,在他的代码的第四行,

newstrides = numpy.zeros(newnd+1).tolist()  # +1 is a fudge

这是一个捏造的因素。这个技巧只在某些情况下有效(并且函数在某些输入上中断)。这是需要的,因为在最外面的while循环结束时递增和设置ni, nj, oi, oj时会出错。更新内容应为:

ni = nj;nj += 1; 
oi = oj;oj += 1;

我认为错误是,因为在原始代码中(在官方numpy github上),它是实现的。

ni = nj++;
 oi = oj++;

使用后增量,而@hoaulj翻译它,就好像使用了前增量,即++nj
为了完整性,我附上了正确的代码波纹管。我希望它能消除任何可能的混乱。

import numpy   # there's an np variable in the code
def attempt_reshape(self, newdims, is_f_order):
    newnd = len(newdims)
    newstrides = numpy.zeros(newnd).tolist()  # +1 is a fudge

    self = numpy.squeeze(self)
    olddims = self.shape
    oldnd = self.ndim
    oldstrides = self.strides

    #/* oi to oj and ni to nj give the axis ranges currently worked with */

    oi,oj = 0,1
    ni,nj = 0,1
    while (ni < newnd) and (oi < oldnd):
        np = newdims[ni];
        op = olddims[oi];
        while (np != op):
            print(ni,oi,np,op,nj,oj)
            if (np < op):
                # /* Misses trailing 1s, these are handled later */
                np *= newdims[nj];
                nj += 1
            else:
                op *= olddims[oj];
                oj += 1

        #/* Check whether the original axes can be combined */
        for ok in range(oi, oj-1):
            if (is_f_order) :
                if (oldstrides[ok+1] != olddims[ok]*oldstrides[ok]):
                    # /* not contiguous enough */
                    return 0;
            else:
                #/* C order */
                if (oldstrides[ok] != olddims[ok+1]*oldstrides[ok+1]) :
                    #/* not contiguous enough */
                    return 0;
        # /* Calculate new strides for all axes currently worked with */
        if (is_f_order) :
            newstrides[ni] = oldstrides[oi];
            for nk in range(ni+1,nj):
                newstrides[nk] = newstrides[nk - 1]*newdims[nk - 1];
        else:
            #/* C order */
            newstrides[nj - 1] = oldstrides[oj - 1];
            #for (nk = nj - 1; nk > ni; nk--) {
            for nk in range(nj-1, ni, -1):
                newstrides[nk - 1] = newstrides[nk]*newdims[nk];
        ni = nj;nj += 1; 
        oi = oj;oj += 1;   

    # * Set strides corresponding to trailing 1s of the new shape.
    if (ni >= 1) :
        last_stride = newstrides[ni - 1];
    else :
        last_stride = self.itemsize # PyArray_ITEMSIZE(self);

    if (is_f_order) :
        last_stride *= newdims[ni - 1];

    for nk in range(ni, newnd):
        newstrides[nk] = last_stride;
    return newstrides

newstrides = attempt_reshape(numpy.zeros((5,3,2)), (10,3), False)

print(newstrides)

相关问题