【问题标题】:Why does Python copy NumPy arrays where the length of the dimensions are the same?为什么 Python 会复制维度长度相同的 NumPy 数组?
【发布时间】:2019-02-20 14:17:56
【问题描述】:

我在引用 NumPy 数组时遇到问题。 我有一个表单数组

import numpy as np
a = [np.array([0.0, 0.2, 0.4, 0.6, 0.8]),
     np.array([0.0, 0.2, 0.4, 0.6, 0.8]),
     np.array([0.0, 0.2, 0.4, 0.6, 0.8])]

如果我现在创建一个新变量,

b = np.array(a)

然后做

b[0] += 1
print(a)

那么a 不会改变。

a = [array([0. , 0.2, 0.4, 0.6, 0.8]),
     array([0. , 0.2, 0.4, 0.6, 0.8]),
     array([0. , 0.2, 0.4, 0.6, 0.8])]

但如果我做同样的事情:

a = [np.array([0.0, 0.2, 0.4, 0.6, 0.8]),
     np.array([0.0, 0.2, 0.4, 0.6, 0.8]),
     np.array([0.0, 0.2, 0.4, 0.6])]

所以我在最后一个维度的末尾删除了一个数字。然后我再次这样做:

b = np.array(a)
b[0] += 1
print(a)

现在a 正在发生变化,我认为这是 Python 中的正常行为。

a = [array([1. , 1.2, 1.4, 1.6, 1.8]),
     array([0. , 0.2, 0.4, 0.6, 0.8]),
     array([0. , 0.2, 0.4, 0.6])]

谁能解释一下?

【问题讨论】:

  • 这是尝试在 NumPy 中制作锯齿状数组或数组数组是一个非常糟糕的主意的原因之一。
  • @user2357112:我宁愿说这是您应该将列表和数组视为概念上不同事物的原因。

标签: python numpy


【解决方案1】:
In [1]: a = [np.array([0.0, 0.2, 0.4, 0.6, 0.8]), 
   ...:      np.array([0.0, 0.2, 0.4, 0.6, 0.8]), 
   ...:      np.array([0.0, 0.2, 0.4, 0.6, 0.8])]                               
In [2]:                                                                         
In [2]: a                                                                       
Out[2]: 
[array([0. , 0.2, 0.4, 0.6, 0.8]),
 array([0. , 0.2, 0.4, 0.6, 0.8]),
 array([0. , 0.2, 0.4, 0.6, 0.8])]

a 是一个数组列表。 b 是一个二维数组。

In [3]: b = np.array(a)                                                         
In [4]: b                                                                       
Out[4]: 
array([[0. , 0.2, 0.4, 0.6, 0.8],
       [0. , 0.2, 0.4, 0.6, 0.8],
       [0. , 0.2, 0.4, 0.6, 0.8]])
In [5]: b[0] += 1                                                               
In [6]: b                                                                       
Out[6]: 
array([[1. , 1.2, 1.4, 1.6, 1.8],
       [0. , 0.2, 0.4, 0.6, 0.8],
       [0. , 0.2, 0.4, 0.6, 0.8]])

ba 获取值,但不包含任何a 对象。这个b 的底层数据结构与a 的列表很不一样。如果不清楚,您可能需要查看 numpy 基础知识(讨论形状、步幅和数据缓冲区)。

在第二种情况下,b 是一个对象数组,包含与a 相同的对象:

In [8]: b = np.array(a)                                                         
In [9]: b                                                                       
Out[9]: 
array([array([0. , 0.2, 0.4, 0.6, 0.8]), array([0. , 0.2, 0.4, 0.6, 0.8]),
       array([0. , 0.2, 0.4, 0.6])], dtype=object)

这个b 的行为很像a - 都包含数组。

这个对象数组的构造与二维数值数组有很大的不同。我认为数值数组是默认的或正常的 numpy 行为,而对象数组是“让步”,为我们提供了一个有用的工具,但它不具备多维数组的计算能力。

很容易错误地创建一个对象数组——有人说太容易了。通过设计可靠地制造一个可能更难。例如原来的a,我们必须这样做:

In [17]: b = np.empty(3, object)                                                
In [18]: b[:] = a[:]                                                            
In [19]: b                                                                      
Out[19]: 
array([array([0. , 0.2, 0.4, 0.6, 0.8]), array([0. , 0.2, 0.4, 0.6, 0.8]),
       array([0. , 0.2, 0.4, 0.6, 0.8])], dtype=object)

甚至for i in range(3): b[i] = a[i]

【讨论】:

    【解决方案2】:

    简而言之,这是您数据的结果。您会注意到这有效/无效(取决于您如何查看),因为您的数组大小不相等

    使用大小相等的子数组,可以将元素紧凑地加载到内存高效方案中,其中任何 N 维数组都可以由内存中的紧凑一维数组表示。 NumPy 然后在内部处理多维索引到一维索引的转换。例如,二维数组的索引 [i, j] 将映射到 i*N + j(如果以行主要格式存储)。原始数组列表中的数据被复制到一个紧凑的一维数组中,因此对该数组所做的任何修改都不会影响原始数组。

    对于参差不齐的列表/数组,这是无法做到的。该数组实际上是一个 Python 列表,其中每个元素都是一个 Python 对象。为了提高效率,只复制对象引用而不复制数据。这就是为什么你可以在第二种情况下改变原始列表元素,但不能在第一种情况下改变。

    【讨论】:

      【解决方案3】:

      在第一种情况下,NumPy 发现numpy.array 的输入可以解释为 3x5 二维数组,因此它会这样做。结果是一个新的 float64 dtype 数组,输入数据复制到其中,与输入对象无关。 b[0]是新数组第一行的视图,完全独立于a[0],修改b[0]不影响a[0]

      在第二种情况下,由于子数组的长度不相等,输入不能解释为二维数组。然而,考虑到子数组是不透明的对象,列表可以解释为对象的一维数组,这是 NumPy 的解释。 numpy.array 调用的结果是对象 dtype 的一维数组,其中包含对作为输入列表元素的数组对象的引用。 b[0] 是与 a[0] 相同的数组对象,b[0] += 1 会改变该对象。

      这种长度依赖性是在 NumPy 中尝试制作锯齿状数组或数组数组是一个非常非常糟糕的主意的众多原因之一。说真的,不要这样做。

      【讨论】:

        【解决方案4】:

        当您创建具有一致长度列表的np.array 时,将创建floats 的新对象np.ndarray

        因此,您的 a[0]b[0] 不共享相同的引用。

        a = [np.array([0.0, 0.2, 0.4, 0.6, 0.8]),
             np.array([0.0, 0.2, 0.4, 0.6, 0.8]),
             np.array([0.0, 0.2, 0.4, 0.6, 0.8])]
        b = np.array(a)
        id(a[0])
        # 139663994327728
        id(b[0])
        # 139663994324672
        

        但是,对于不同长度的列表,np.array 创建 np.ndarray 并以 object 作为其元素。

        a2 = [np.array([0. , 0.2, 0.4, 0.6, 0.8]), 
             np.array([0. , 0.2, 0.4, 0.6, 0.8]), 
             np.array([0. , 0.2, 0.4, 0.6])]
        b2 = np.array(a2)
        b2
        array([array([1. , 1.2, 1.4, 1.6, 1.8]), array([0. , 0.2, 0.4, 0.6, 0.8]),
               array([0. , 0.2, 0.4, 0.6])], dtype=object)
        

        b2 仍然保留来自a2 的相同引用:

        for s in a2:
            print(id(s))
        # 139663994330128
        # 139663994328448
        # 139663994329488
        
        for s in b2:
            print(id(s))
        # 139663994330128
        # 139663994328448
        # 139663994329488
        

        除了a2[0] 之外,还有b2[0] 结果。

        【讨论】:

          【解决方案5】:

          @coldspeed 正确解释了您看到行为差异的原因。我只是想指出复制是预期的。

          documentation 中你可以看到,该函数有一个复制标志,默认设置为True

          numpy.array(object, dtype=None, copy=True, order='K', subok=False, ndmin=0)
          

          如果只在必要时进行复制,请改用np.asarray

          在您的示例中,这并没有真正的区别,因为 a 是一个列表而不是一个 numpy 数组,所以它总是会被复制。

          如果a 是一个数组,则行为如下:

          import numpy as np
          a = np.array([[0.0, 0.2, 0.4, 0.6, 0.8],
                        [0.0, 0.2, 0.4, 0.6, 0.8],
                        [0.0, 0.2, 0.4, 0.6, 0.8]])
          b=np.array(a)
          b[0] += 1
          a
          
          Out[6]: 
          array([[0. , 0.2, 0.4, 0.6, 0.8],
                 [0. , 0.2, 0.4, 0.6, 0.8],
                 [0. , 0.2, 0.4, 0.6, 0.8]])
          c = np.asarray(a)
          c[0] +=1
          a
          
          Out[9]: 
          array([[1. , 1.2, 1.4, 1.6, 1.8],
                 [0. , 0.2, 0.4, 0.6, 0.8],
                 [0. , 0.2, 0.4, 0.6, 0.8]])
          

          【讨论】:

            【解决方案6】:

            numpy.array() 设计的主要用例是创建一个 n 维数字数组,其中所有数字都存储在 numpy 自己高效设计的内部结构中。

            只要可能这样做,numpy.array() 确实会这样做。

            (这种内部结构的效率将是您使用 numpy ndarrays 而不是 Python 列表的主要原因,因此复制数字这一事实实际上对您来说应该是一件可取的/好事)

            当您的 a 是 3 个 ndarrays 的列表,每个大小为 5 时,numpy.array() 显然可能创建一个 n 维 ndarray 数字(特别是二维一、形状为(3,5))。

            所以,对b[0] 的任何更改实际上都是对这个数字内部数据结构的更改,这些数字都是从a 复制过来的。

            当您的 a 是大小不等的 ndarray 列表时,numpy.array() 不再可能将其转换为形状为 (3,5) 的 n 维数组。

            因此,该函数做了它可以做的下一件最好的事情,即将 3 个 ndarray 中的每一个都视为 object,并返回这些 objects 的一维 ndarray。这个返回的 ndarray 的长度是3objects 的数量)。你可以通过打印b.shape(将打印(3,)而不是(3,5))和b.dtype(将打印object而不是float64)看到这一点。

            在这种情况下,numpy.array() 不会深入您的 3 个 ndarray 中的每一个来复制这 3 个 ndarray 的数字,因为它不会创建自己的高效设计的 n 维数字数组——它只会返回一个objects 的一维数组。

            因此,您对b[0] 所做的任何更改也可以通过a 看到,因为ab 都持有对相同objects 的引用(3 个大小不等的ndarray)。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2017-07-13
              • 2020-10-29
              • 2015-08-24
              • 2018-04-14
              相关资源
              最近更新 更多