【问题标题】:When will numpy copy the array when using reshape()使用 reshape() 时 numpy 何时复制数组
【发布时间】:2016-05-03 03:50:37
【问题描述】:

numpy.reshape的文档中说:

如果可能,这将是一个新的视图对象;否则,它将是一个副本。请注意,不能保证返回数组的内存布局(C 或 Fortran 连续)。

我的问题是,numpy 什么时候选择返回一个新视图,什么时候复制整个数组?是否有任何一般原则告诉人们reshape 的行为,或者它只是不可预测的?谢谢。

【问题讨论】:

  • 对我来说,这看起来像是 stackoverflow.com/q/11524664/748858 的欺骗......
  • 我认为你是否有一个连续的内存块开始是你是否会获得视图或副本的一个重要因素......
  • 您的链接更多地关注“我如何测试”,而不是我为什么或如何预测它。
  • @hpaulj 提供的代码中有一个错误。我附上了为什么上述代码有问题的解释,并提供了更正的实现。

标签: python numpy


【解决方案1】:

@mgillson 发现的链接似乎解决了“我如何判断它是否制作了副本”的问题,而不是“我如何预测它”或理解它制作副本的原因。至于测试,我喜欢用A.__array_interfrace__

如果您尝试将值分配给重整后的数组,并希望同时更改原始数组,这很可能会成为问题。而且我很难找到有问题的 SO 案例。

复制重塑会比非复制重塑慢一点,但我再次想不出会导致整个代码变慢的情况。如果您使用的数组太大以至于最简单的操作会产生内存错误,那么副本也可能是一个问题。


在重塑数据缓冲区中的值后,需要以连续的顺序排列,“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,转置,具有相同的“数据”指针。转置是在不更改或复制数据的情况下执行的,它只是使用新的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)}

拉开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)

像这样将值分配给一个 raveled 数组可能是最有可能的情况,其中副本会产生错误。例如,x.ravel()[::2]=99 每隔一个值更改xy(分别为列和行)。但是y.ravel()[::2]=0 因为这个抄袭什么都没做。

所以转置后重塑是最有可能的复制方案。我很乐意探索其他可能性。

编辑: y.reshape(-1,order='F')[::2]=0 确实改变了 y 的值。使用兼容的顺序,reshape 不会生成副本。


@mgillson 链接中的一个答案https://stackoverflow.com/a/14271298/901925 指出A.shape=... 语法可以防止复制。如果不复制就不能改变形状会报错:

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

reshape 文档中也提到了这一点

如果您希望在复制数据时引发错误, 您应该将新形状分配给数组的形状属性::


关于重塑以下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

【讨论】:

  • 非常感谢您的出色回答。其实我也大致知道连续是阻止reshape复制的条件之一。但我只是不确定它们之间的关系是否恰好是“当且仅当”的关系。
  • reshape 在编译后的代码中实现。准确理解 numpy c 代码中发生的事情需要一些挖掘和研究。
  • numpy/core/src/multiarray/shape.c
  • 特别是_attempt_nocopy_reshape函数...
  • @hpaulj 你提供了一个非常好的和详细的答案。但是您的最终实现中存在一个错误,这就是您需要在第 4 行中使用 +1 软糖因子的原因。我提供了一个答案,其中解释了错误并附上了代码的更正版本。我希望这是可以接受的。
【解决方案2】:

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

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

有软糖因素。此 hack 仅在某些情况下有效(并且该功能在某些输入上中断)。需要 hack,因为在最外层 while 循环的末尾递增和设置 ni, nj, oi, oj 时会出错。更新应该是

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

我认为错误来了,因为在原始代码(on official 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)

【讨论】:

    【解决方案3】:

    您可以通过仅测试相关维度的连续性来进行预测。

    Here is the code numpy 决定使用视图还是副本。)

    连续性是指任意维度的stride正好等于next-faster-variable维度的stride × length

    例如,涉及的意思是,最内层和最外层维度是否不连续并不重要,只要保持它们相同即可。


    一般来说,如果你所做的只是重塑整个数组,你可以期待一个视图。如果您正在处理更大数组的子集,或者以任何方式对元素进行了重新排序,那么很可能是副本。


    例如,考虑一个矩阵:

    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 属性来确认)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-02-15
      • 2015-07-05
      • 1970-01-01
      • 2021-07-24
      • 1970-01-01
      • 1970-01-01
      • 2021-03-01
      相关资源
      最近更新 更多