比较 NumPy 对象引用

Comparing NumPy object references

我想了解 NumPy 的行为。

当我尝试获取 NumPy 数组的内部数组的引用,然后将其与对象本身进行比较时,我得到了返回值 False

示例如下:

In [198]: x = np.array([[1,2,3], [4,5,6]])
In [201]: x0 = x[0]
In [202]: x0 is x[0]
Out[202]: False

而另一方面,对于 Python 本机对象,返回的是 True

In [205]: c = [[1,2,3],[1]]    
In [206]: c0 = c[0]    
In [207]: c0 is c[0]
Out[207]: True

我的问题是,这是 NumPy 的预期行为吗?如果是这样,我想创建一个NumPy数组内部对象的引用怎么办

我认为您对 Numpy 数组的理解有误。您认为 Numpy 中多维数组中的子数组(如 Python 列表中的子数组)是独立的对象,嗯,它们不是。

一个Numpy数组,无论其维度如何,都只是一个对象。这是因为 Numpy 在 C 级别创建数组,当将它们作为 python 对象加载时,它不能分解为多个对象。这使得 Python 在使用 split()__getitem__take() 等属性时创建一个新对象来保存新部分,事实上,它只是 python 抽象 Numpy 数组的类列表行为的方式。

您还可以像下面这样实时检查瘦身:

In [7]: x
Out[7]: 
array([[1, 2, 3],
       [4, 5, 6]])

In [8]: x[0] is x[0]
Out[8]: False

因此,只要您拥有一个数组或任何可以在其中包含其他对象的可变对象,您就会拥有一个 python 可变对象,因此您将失去性能和所有其他 Numpy 数组的酷炫功能.

另外,正如@Imanol 在评论中提到的那样,如果您想在使用引用修改数组时进行内存优化和灵活操作,则可能需要使用 Numpy 视图对象。 view 对象可以通过以下两种方式构造:

a.view(some_dtype) or a.view(dtype=some_dtype) constructs a view of the array’s memory with a different data-type. This can cause a reinterpretation of the bytes of memory.

a.view(ndarray_subclass) or a.view(type=ndarray_subclass) just returns an instance of ndarray_subclass that looks at the same array (same shape, dtype, etc.) This does not cause a reinterpretation of the memory.

For a.view(some_dtype), if some_dtype has a different number of bytes per entry than the previous dtype (for example, converting a regular array to a structured array), then the behavior of the view cannot be predicted just from the superficial appearance of a (shown by print(a)). It also depends on exactly how a is stored in memory. Therefore if a is C-ordered versus fortran-ordered, versus defined as a slice or transpose, etc., the view may give different results.

二维切片

当我第一次写这篇文章时,我构建并索引了一个一维数组。但是 OP 使用的是二维数组,所以 x[0] 是 'row',原始的一部分。

In [81]: arr = np.array([[1,2,3], [4,5,6]])
In [82]: arr.__array_interface__['data']
Out[82]: (181595128, False)

In [83]: x0 = arr[0,:]
In [84]: x0.__array_interface__['data']
Out[84]: (181595128, False)        # same databuffer pointer
In [85]: id(x0)
Out[85]: 2886887088
In [86]: x1 = arr[0,:]             # another slice, different id
In [87]: x1.__array_interface__['data']
Out[87]: (181595128, False)
In [88]: id(x1)
Out[88]: 2886888888

我之前写的关于切片的内容仍然适用。索引单个元素,如 arr[0,0] 与一维数组相同。

此 2d arr 与 1d arr.ravel() 具有相同的数据缓冲区;形状和步幅不同。 viewcopyitem 之间的区别仍然适用。

在 C 中实现二维数组的一种常见方法是使用一个指向其他数组的指针数组。 numpy 采用一种不同的 strided 方法,只有一个平面数据数组,并使用 shapestrides 参数来实现横向。所以一个子数组需要它自己的 shapestrides 以及指向共享数据缓冲区的指针。

1d 数组索引

我将尝试说明索引数组时发生的情况:

In [51]: arr = np.arange(4)

数组是一个对象,具有各种属性,例如形状和数据缓冲区。缓冲区将数据存储为字节(在 C 数组中),而不是 Python 数字对象。您可以通过以下方式查看有关阵列的信息:

In [52]: np.info(arr)
class:  ndarray
shape:  (4,)
strides:  (4,)
itemsize:  4
aligned:  True
contiguous:  True
fortran:  True
data pointer: 0xa84f8d8
byteorder:  little
byteswap:  False
type: int32

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

一个是十六进制的数据指针,另一个是十进制的。我们通常不会直接引用它。

如果我索引一个元素,我得到一个新对象:

In [54]: x1 = arr[1]
In [55]: type(x1)
Out[55]: numpy.int32
In [56]: x1.__array_interface__
Out[56]: 
{'__ref': array(1),
 'data': (181158400, False),
....}
In [57]: id(x1)
Out[57]: 2946170352

它具有数组的一些属性,但不是全部。例如,您不能分配给它。还要注意它的“数据”值是完全不同的。

从同一个地方进行另一个选择 - 不同的 ID 和不同的数据:

In [58]: x2 = arr[1]
In [59]: id(x2)
Out[59]: 2946170336
In [60]: x2.__array_interface__['data']
Out[60]: (181143288, False)

此外,如果我此时更改数组,也不会影响之前的选择:

In [61]: arr[1] = 10
In [62]: arr
Out[62]: array([ 0, 10,  2,  3])
In [63]: x1
Out[63]: 1

x1x2 没有相同的 id,因此不会与 is 匹配,并且它们不使用 arr数据缓冲区。没有记录表明这两个变量都来自 arr.

使用slicing可以得到原始数组的view

In [64]: y = arr[1:2]
In [65]: y.__array_interface__
Out[65]: 
{'data': (176486620, False),
 'descr': [('', '<i4')],
 'shape': (1,),
 ....}
In [66]: y
Out[66]: array([10])
In [67]: y[0]=4
In [68]: arr
Out[68]: array([0, 4, 2, 3])
In [69]: x1
Out[69]: 1

它的数据指针比 arr 大 4 个字节 - 也就是说,它指向相同的缓冲区,只是不同的位置。并且更改 y 确实会更改 arr(但不会更改独立的 x1)。

我什至可以对这个项目进行 0d 视图

In [71]: z = y.reshape(())
In [72]: z
Out[72]: array(4)
In [73]: z[...]=0
In [74]: arr
Out[74]: array([0, 0, 2, 3])

在 Python 代码中,我们通常不使用这样的对象。当我们使用 c-apicython 时是否可以直接访问数据缓冲区。 nditer 是一种迭代机制,适用于像这样的 0d 对象(在 Python 或 c-api 中)。在 cython typed memoryviews 中对于低级别访问特别有用。

http://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html

https://docs.scipy.org/doc/numpy/reference/arrays.nditer.html

https://docs.scipy.org/doc/numpy/reference/c-api.iterator.html#c.NpyIter

逐元素==

回复评论,

np.array([1]) == np.array([2]) will return array([False], dtype=bool)

== 为数组定义为逐元素操作。它比较各个元素的值和 returns 匹配的布尔数组。

如果需要在标量上下文(例如 if)中使用此类比较,则需要将其简化为单个值,如 np.allnp.any .

is 测试比较对象 ID(不仅仅是 numpy 对象)。它在实际编码中的价值有限。我最常在像 is None 这样的表达式中使用它,其中 None 是一个具有唯一 ID 的对象,并且不能很好地进行相等性测试。

不确定此时是否有用,但 numpy.ndarray.ctypes 似乎有一些有用的信息: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.ctypes.html

使用了类似的东西(缺少 dtype,但是嗯):

def is_same_array(a, b):
    return (a.shape == b.shape) and (a == b).all() and a.ctypes.data == b.ctypes.data

此处: https://github.com/EricCousineau-TRI/repro/blob/a60daf899e9726daf2ca1259bb80ad2c7c9b3e3f/python/namedlist_alt.py#L111