np 数组的地址传递、view 与 copy

前言

numpy 是 python 众多第三方模块中非常实用高效的一个,但是在使用 numpy 的过程中,我也遇到了不少问题。其中这个 np 数组的赋值方式就十分令人困惑。

在使用 python 的 list 时,如果对一个 list 进行切片,那么返回的就是一个新的 list 。例如下面的代码:

>>> a = [1, 2, 3]
>>> b = a[:]
>>> b[1] = 999
>>> a
[1, 2, 3]

此时, ba 的一个复制,对 b 进行修改时不会影响 a 中元素的值。

但是这个特性在 np 数组中就不成立了。

>>> a = np.array([1, 2, 3])
>>> b = a[:]
>>> b[1] = 999
>>> a
array([  1, 999,   3])

因为这个原因,我之前一直无法找出某个程序的错误,后来才知道 在 np 数组中切片并不是对原数组进行 copy ,而是一个进行了 view 的赋值方法。

因此,我打算在这篇博客中深入区分一下 numpy 数组的三种赋值方式,防止再因为不同的赋值方式导致程序出错。

1. 地址的传递

赋值时,python 只会将原 numpy 数组的地址传递给另一个变量

>>> a = np.arange(12)
>>> a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])
>>> b = a
>>> b is a
True

此时, ba 的地址相同,指向同一个 np 数组。

如果在这个时候改变 b 的大小,那么 a 的大小也会改变。

>>> b.shape = 3, 4
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> a.shape
(3, 4)

将 np 数组作为函数参数传入时,也会直接将地址传入。

>>> def f(x):
     print(id(x))

>>> id(a)
4466696320
>>> f(a)
4466696320

2. view

view 函数可以创建一个一一对应的新 np 数组。如果对 view 中的数据进行修改,原数组中对应位置的数据也会修改

>>> c = a.view()
>>> c is a
False
>>> c.base is a
True

其中 c.base 指向的就为原始数据。

此时,如果改变 c 的形状, a 的大小并不会发生改变。但是如果我们修改其中的某个位置的元素值时,在 a 中对应位置的元素也会变化

>>> c.shape = 2, 6
>>> a.shape
(3, 4)
>>> c[0, 4] = 1234
>>> a
array([[   0,    1,    2,    3],
       [1234,    5,    6,    7],
       [   8,    9,   10,   11]])

如果对一个 numpy 数组进行切片时,返回的是一个 view

>>> s = a[:, 1:3]
>>> s[:] = 10
>>> a
array([[   0,   10,   10,    3],
       [1234,   10,   10,    7],
       [   8,   10,   10,   11]])

3. copy

使用 copy 函数可以完全拷贝原数组中的所有值。

>>> d = a.copy()
>>> d is a
False
>>> d.base is a
False
>>> d[0,0] = 9999
>>> a
array([[   0,   10,   10,    3],
       [1234,   10,   10,    7],
       [   8,   10,   10,   11]])

结语

无论是指针传递、 view 还是 copy ,其实他们都具有各自的优缺点。在使用时我们也需要灵活地选择三种赋值方法,并且小心使用不恰当的方法所带来的意料之外的错误。